keycloak-aplcache

Changes

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
index 452396a..d85c9d4 100644
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/PolicyEnforcer.java
@@ -225,9 +225,10 @@ public class PolicyEnforcer {
             for (String id : protectedResource.findAll()) {
                 ResourceRepresentation resourceDescription = protectedResource.findById(id);
 
-                if (resourceDescription.getUri() != null) {
-                    PathConfig pathConfig = PathConfig.createPathConfig(resourceDescription);
-                    paths.put(pathConfig.getPath(), pathConfig);
+                if (resourceDescription.getUris() != null && !resourceDescription.getUris().isEmpty()) {
+                    for(PathConfig pathConfig : PathConfig.createPathConfigs(resourceDescription)) {
+                        paths.put(pathConfig.getPath(), pathConfig);
+                    }
                 }
             }
         }
@@ -277,7 +278,7 @@ public class PolicyEnforcer {
                                 cipConfig = pathConfig.getClaimInformationPointConfig();
                             }
 
-                            pathConfig = PathConfig.createPathConfig(matchingResources.get(0));
+                            pathConfig = PathConfig.createPathConfigs(matchingResources.get(0)).iterator().next();
 
                             if (cipConfig != null) {
                                 pathConfig.setClaimInformationPointConfig(cipConfig);
@@ -323,7 +324,7 @@ public class PolicyEnforcer {
 
                 if (!search.isEmpty()) {
                     ResourceRepresentation targetResource = search.get(0);
-                    PathConfig config = PathConfig.createPathConfig(targetResource);
+                    PathConfig config = PathConfig.createPathConfigs(targetResource).iterator().next();
 
                     config.setScopes(originalConfig.getScopes());
                     config.setMethods(originalConfig.getMethods());
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
index ae448d8..b6872ec 100644
--- a/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/PolicyEnforcerConfig.java
@@ -27,6 +27,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.ScopeRepresentation;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
@@ -116,30 +119,35 @@ public class PolicyEnforcerConfig {
 
     public static class PathConfig {
 
-        public static PathConfig createPathConfig(ResourceRepresentation resourceDescription) {
-            PathConfig pathConfig = new PathConfig();
+        public static Set<PathConfig> createPathConfigs(ResourceRepresentation resourceDescription) {
+            Set<PathConfig> pathConfigs = new HashSet<>();
 
-            pathConfig.setId(resourceDescription.getId());
-            pathConfig.setName(resourceDescription.getName());
+            for (String uri : resourceDescription.getUris()) {
 
-            String uri = resourceDescription.getUri();
+                PathConfig pathConfig = new PathConfig();
 
-            if (uri == null || "".equals(uri.trim())) {
-                throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
-            }
+                pathConfig.setId(resourceDescription.getId());
+                pathConfig.setName(resourceDescription.getName());
 
-            pathConfig.setPath(uri);
+                if (uri == null || "".equals(uri.trim())) {
+                    throw new RuntimeException("Failed to configure paths. Resource [" + resourceDescription.getName() + "] has an invalid or empty URI [" + uri + "].");
+                }
 
-            List<String> scopeNames = new ArrayList<>();
+                pathConfig.setPath(uri);
 
-            for (ScopeRepresentation scope : resourceDescription.getScopes()) {
-                scopeNames.add(scope.getName());
-            }
+                List<String> scopeNames = new ArrayList<>();
 
-            pathConfig.setScopes(scopeNames);
-            pathConfig.setType(resourceDescription.getType());
+                for (ScopeRepresentation scope : resourceDescription.getScopes()) {
+                    scopeNames.add(scope.getName());
+                }
+
+                pathConfig.setScopes(scopeNames);
+                pathConfig.setType(resourceDescription.getType());
+
+                pathConfigs.add(pathConfig);
+            }
 
-            return pathConfig;
+            return pathConfigs;
         }
 
         private String name;
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
index 92f4717..24a1be7 100644
--- a/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java
@@ -44,7 +44,9 @@ public class ResourceRepresentation {
     private String id;
 
     private String name;
-    private String uri;
+
+    @JsonProperty("uris")
+    private Set<String> uris;
     private String type;
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     @JsonProperty("scopes")
@@ -64,29 +66,37 @@ public class ResourceRepresentation {
      * Creates a new instance.
      *
      * @param name a human-readable string describing a set of one or more resources
-     * @param uri a {@link URI} that provides the network location for the resource set being registered
+     * @param uris a {@link List} of {@link URI} that provides network locations for the resource set being registered
      * @param type a string uniquely identifying the semantics of the resource set
      * @param scopes the available scopes for this resource set
      * @param iconUri a {@link URI} for a graphic icon representing the resource set
      */
-    public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type, String iconUri) {
+    public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, Set<String> uris, String type, String iconUri) {
         this.name = name;
         this.scopes = scopes;
-        this.uri = uri;
+        this.uris = uris;
         this.type = type;
         this.iconUri = iconUri;
     }
 
+    public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type, String iconUri) {
+        this(name, scopes, Collections.singleton(uri), type, iconUri);
+    }
+
     /**
      * Creates a new instance.
      *
      * @param name a human-readable string describing a set of one or more resources
-     * @param uri a {@link URI} that provides the network location for the resource set being registered
+     * @param uris a {@link List} of {@link URI} that provides the network location for the resource set being registered
      * @param type a string uniquely identifying the semantics of the resource set
      * @param scopes the available scopes for this resource set
      */
+    public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, Set<String> uris, String type) {
+        this(name, scopes, uris, type, null);
+    }
+
     public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes, String uri, String type) {
-        this(name, scopes, uri, type, null);
+        this(name, scopes, Collections.singleton(uri), type, null);
     }
 
     /**
@@ -97,7 +107,7 @@ public class ResourceRepresentation {
      * @param scopes the available scopes for this resource set
      */
     public ResourceRepresentation(String name, Set<ScopeRepresentation> scopes) {
-        this(name, scopes, null, null, null);
+        this(name, scopes, (Set<String>) null, null, null);
     }
 
     public ResourceRepresentation(String name, String... scopes) {
@@ -114,7 +124,7 @@ public class ResourceRepresentation {
      *
      */
     public ResourceRepresentation() {
-        this(null, null, null, null, null);
+        this(null, null, (Set<String>) null, null, null);
     }
 
     public void setId(String id) {
@@ -133,8 +143,18 @@ public class ResourceRepresentation {
         return displayName;
     }
 
+    @Deprecated
+    @JsonIgnore
     public String getUri() {
-        return this.uri;
+        if (this.uris == null || this.uris.isEmpty()) {
+            return null;
+        }
+
+        return this.uris.iterator().next();
+    }
+
+    public Set<String> getUris() {
+        return this.uris;
     }
 
     public String getType() {
@@ -161,12 +181,35 @@ public class ResourceRepresentation {
         this.displayName = displayName;
     }
 
+    @Deprecated
     public void setUri(String uri) {
         if (uri != null && !"".equalsIgnoreCase(uri.trim())) {
-            this.uri = uri;
+            this.uris = Collections.singleton(uri);
+        }
+    }
+
+    public void setUris(Set<String> uris) {
+        if (uris != null) {
+            Set<String> resultSet = new HashSet<>();
+            for (String uri : uris) {
+                if (uri != null && !"".equalsIgnoreCase(uri.trim())) {
+                    resultSet.add(uri);
+                }
+            }
+
+            this.uris = resultSet;
         }
     }
 
+    @JsonProperty("uri")
+    public void addUri(String uri) {
+        if (this.uris == null) {
+            this.uris = new HashSet<>();
+        }
+
+        uris.add(uri);
+    }
+
     public void setType(String type) {
         if (type != null && !"".equalsIgnoreCase(type.trim())) {
             this.type = type;
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
index 7e75410..c631279 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java
@@ -39,7 +39,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
     private String type;
     private String name;
     private String displayName;
-    private String uri;
+    private Set<String> uris;
     private Set<String> scopesIds;
     private boolean ownerManagedAccess;
     private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
@@ -48,7 +48,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
         super(revision, resource.getId());
         this.name = resource.getName();
         this.displayName = resource.getDisplayName();
-        this.uri = resource.getUri();
+        this.uris = resource.getUris();
         this.type = resource.getType();
         this.owner = resource.getOwner();
         this.iconUri = resource.getIconUri();
@@ -67,8 +67,8 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
         return this.displayName;
     }
 
-    public String getUri() {
-        return this.uri;
+    public Set<String> getUris() {
+        return this.uris;
     }
 
     public String getType() {
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceRemovedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceRemovedEvent.java
index 6243240..0b30046 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceRemovedEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceRemovedEvent.java
@@ -41,15 +41,15 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
     private String owner;
     private String serverId;
     private String type;
-    private String uri;
+    private Set<String> uris;
     private Set<String> scopes;
 
-    public static ResourceRemovedEvent create(String id, String name, String type, String uri, String owner, Set<String> scopes, String serverId) {
+    public static ResourceRemovedEvent create(String id, String name, String type, Set<String> uris, String owner, Set<String> scopes, String serverId) {
         ResourceRemovedEvent event = new ResourceRemovedEvent();
         event.id = id;
         event.name = name;
         event.type = type;
-        event.uri = uri;
+        event.uris = uris;
         event.owner = owner;
         event.scopes = scopes;
         event.serverId = serverId;
@@ -68,7 +68,7 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
 
     @Override
     public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
-        cache.resourceRemoval(id, name, type, uri, owner, scopes, serverId, invalidations);
+        cache.resourceRemoval(id, name, type, uris, owner, scopes, serverId, invalidations);
     }
 
     public static class ExternalizerImpl implements Externalizer<ResourceRemovedEvent> {
@@ -82,7 +82,7 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
             MarshallUtil.marshallString(obj.id, output);
             MarshallUtil.marshallString(obj.name, output);
             MarshallUtil.marshallString(obj.type, output);
-            MarshallUtil.marshallString(obj.uri, output);
+            KeycloakMarshallUtil.writeCollection(obj.uris, KeycloakMarshallUtil.STRING_EXT, output);
             MarshallUtil.marshallString(obj.owner, output);
             KeycloakMarshallUtil.writeCollection(obj.scopes, KeycloakMarshallUtil.STRING_EXT, output);
             MarshallUtil.marshallString(obj.serverId, output);
@@ -103,7 +103,7 @@ public class ResourceRemovedEvent extends InvalidationEvent implements Authoriza
             res.id = MarshallUtil.unmarshallString(input);
             res.name = MarshallUtil.unmarshallString(input);
             res.type = MarshallUtil.unmarshallString(input);
-            res.uri = MarshallUtil.unmarshallString(input);
+            res.uris =  KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, HashSet::new);
             res.owner = MarshallUtil.unmarshallString(input);
             res.scopes = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, HashSet::new);
             res.serverId = MarshallUtil.unmarshallString(input);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceUpdatedEvent.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceUpdatedEvent.java
index fcd9987..10bfb64 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceUpdatedEvent.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/events/ResourceUpdatedEvent.java
@@ -40,16 +40,16 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
     private String name;
     private String serverId;
     private String type;
-    private String uri;
+    private Set<String> uris;
     private Set<String> scopes;
     private String owner;
 
-    public static ResourceUpdatedEvent create(String id, String name, String type, String uri, Set<String> scopes, String serverId, String owner) {
+    public static ResourceUpdatedEvent create(String id, String name, String type, Set<String> uris, Set<String> scopes, String serverId, String owner) {
         ResourceUpdatedEvent event = new ResourceUpdatedEvent();
         event.id = id;
         event.name = name;
         event.type = type;
-        event.uri = uri;
+        event.uris = uris;
         event.scopes = scopes;
         event.serverId = serverId;
         event.owner = owner;
@@ -68,7 +68,7 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
 
     @Override
     public void addInvalidations(StoreFactoryCacheManager cache, Set<String> invalidations) {
-        cache.resourceUpdated(id, name, type, uri, scopes, serverId, owner, invalidations);
+        cache.resourceUpdated(id, name, type, uris, scopes, serverId, owner, invalidations);
     }
 
     public static class ExternalizerImpl implements Externalizer<ResourceUpdatedEvent> {
@@ -82,7 +82,7 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
             MarshallUtil.marshallString(obj.id, output);
             MarshallUtil.marshallString(obj.name, output);
             MarshallUtil.marshallString(obj.type, output);
-            MarshallUtil.marshallString(obj.uri, output);
+            KeycloakMarshallUtil.writeCollection(obj.uris, KeycloakMarshallUtil.STRING_EXT, output);
             KeycloakMarshallUtil.writeCollection(obj.scopes, KeycloakMarshallUtil.STRING_EXT, output);
             MarshallUtil.marshallString(obj.serverId, output);
             MarshallUtil.marshallString(obj.owner, output);
@@ -103,7 +103,7 @@ public class ResourceUpdatedEvent extends InvalidationEvent implements Authoriza
             res.id = MarshallUtil.unmarshallString(input);
             res.name = MarshallUtil.unmarshallString(input);
             res.type = MarshallUtil.unmarshallString(input);
-            res.uri = MarshallUtil.unmarshallString(input);
+            res.uris = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, HashSet::new);
             res.scopes = KeycloakMarshallUtil.readCollection(input, KeycloakMarshallUtil.STRING_EXT, HashSet::new);
             res.serverId = MarshallUtil.unmarshallString(input);
             res.owner = MarshallUtil.unmarshallString(input);
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
index e6ec3d8..d200234 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java
@@ -49,7 +49,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     @Override
     public Resource getDelegateForUpdate() {
         if (updated == null) {
-            cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+            cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
             updated = cacheSession.getResourceStoreDelegate().findById(cached.getId(), cached.getResourceServerId());
             if (updated == null) throw new IllegalStateException("Not found in database");
         }
@@ -98,7 +98,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     @Override
     public void setName(String name) {
         getDelegateForUpdate();
-        cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+        cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
         updated.setName(name);
     }
 
@@ -111,7 +111,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     @Override
     public void setDisplayName(String name) {
         getDelegateForUpdate();
-        cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+        cacheSession.registerResourceInvalidation(cached.getId(), name, cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
         updated.setDisplayName(name);
     }
 
@@ -134,16 +134,16 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     }
 
     @Override
-    public String getUri() {
-        if (isUpdated()) return updated.getUri();
-        return cached.getUri();
+    public Set<String> getUris() {
+        if (isUpdated()) return updated.getUris();
+        return cached.getUris();
     }
 
     @Override
-    public void setUri(String uri) {
+    public void updateUris(Set<String> uris) {
         getDelegateForUpdate();
-        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), uri, cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
-        updated.setUri(uri);
+        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), uris, cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+        updated.updateUris(uris);
     }
 
     @Override
@@ -155,7 +155,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     @Override
     public void setType(String type) {
         getDelegateForUpdate();
-        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), type, cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), type, cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
         updated.setType(type);
 
     }
@@ -189,7 +189,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
     @Override
     public void setOwnerManagedAccess(boolean ownerManagedAccess) {
         getDelegateForUpdate();
-        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
+        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), cached.getScopesIds(), cached.getResourceServerId(), cached.getOwner());
         updated.setOwnerManagedAccess(ownerManagedAccess);
     }
 
@@ -208,7 +208,7 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
             }
         }
 
-        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUri(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner());
+        cacheSession.registerResourceInvalidation(cached.getId(), cached.getName(), cached.getType(), cached.getUris(), scopes.stream().map(scope1 -> scope1.getId()).collect(Collectors.toSet()), cached.getResourceServerId(), cached.getOwner());
         updated.updateScopes(scopes);
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
index c3a1424..8ee94ea 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheManager.java
@@ -77,7 +77,7 @@ public class StoreFactoryCacheManager extends CacheManager {
         addInvalidations(InScopePredicate.create().scope(id), invalidations);
     }
 
-    public void resourceUpdated(String id, String name, String type, String uri, Set<String> scopes, String serverId, String owner, Set<String> invalidations) {
+    public void resourceUpdated(String id, String name, String type, Set<String> uris, Set<String> scopes, String serverId, String owner, Set<String> invalidations) {
         invalidations.add(id);
         invalidations.add(StoreFactoryCacheSession.getResourceByNameCacheKey(name, owner, serverId));
         invalidations.add(StoreFactoryCacheSession.getResourceByOwnerCacheKey(owner, serverId));
@@ -89,8 +89,10 @@ public class StoreFactoryCacheManager extends CacheManager {
             addInvalidations(InResourcePredicate.create().resource(type), invalidations);
         }
 
-        if (uri != null) {
-            invalidations.add(StoreFactoryCacheSession.getResourceByUriCacheKey(uri, serverId));
+        if (uris != null) {
+            for (String uri: uris) {
+                invalidations.add(StoreFactoryCacheSession.getResourceByUriCacheKey(uri, serverId));
+            }
         }
 
         if (scopes != null) {
@@ -101,8 +103,8 @@ public class StoreFactoryCacheManager extends CacheManager {
         }
     }
 
-    public void resourceRemoval(String id, String name, String type, String uri, String owner, Set<String> scopes, String serverId, Set<String> invalidations) {
-        resourceUpdated(id, name, type, uri, scopes, serverId, owner, invalidations);
+    public void resourceRemoval(String id, String name, String type, Set<String> uris, String owner, Set<String> scopes, String serverId, Set<String> invalidations) {
+        resourceUpdated(id, name, type, uris, scopes, serverId, owner, invalidations);
         addInvalidations(InResourcePredicate.create().resource(id), invalidations);
     }
 
diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
index 06e9f25..95e1d32 100644
--- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/StoreFactoryCacheSession.java
@@ -264,12 +264,12 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
         invalidationEvents.add(ScopeUpdatedEvent.create(id, name, serverId));
     }
 
-    public void registerResourceInvalidation(String id, String name, String type, String uri, Set<String> scopes, String serverId, String owner) {
-        cache.resourceUpdated(id, name, type, uri, scopes, serverId, owner, invalidations);
+    public void registerResourceInvalidation(String id, String name, String type, Set<String> uris, Set<String> scopes, String serverId, String owner) {
+        cache.resourceUpdated(id, name, type, uris, scopes, serverId, owner, invalidations);
         ResourceAdapter adapter = managedResources.get(id);
         if (adapter != null) adapter.invalidateFlag();
 
-        invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uri, scopes, serverId, owner));
+        invalidationEvents.add(ResourceUpdatedEvent.create(id, name, type, uris, scopes, serverId, owner));
     }
 
     public void registerPolicyInvalidation(String id, String name, Set<String> resources, Set<String> scopes, String defaultResourceType, String serverId) {
@@ -550,7 +550,7 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
         public Resource create(String id, String name, ResourceServer resourceServer, String owner) {
             Resource resource = getResourceStoreDelegate().create(id, name, resourceServer, owner);
             Resource cached = findById(resource.getId(), resourceServer.getId());
-            registerResourceInvalidation(resource.getId(), resource.getName(), resource.getType(), resource.getUri(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resourceServer.getId(), resource.getOwner());
+            registerResourceInvalidation(resource.getId(), resource.getName(), resource.getType(), resource.getUris(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resourceServer.getId(), resource.getOwner());
             return cached;
         }
 
@@ -561,8 +561,8 @@ public class StoreFactoryCacheSession implements CachedStoreFactoryProvider {
             if (resource == null) return;
 
             cache.invalidateObject(id);
-            invalidationEvents.add(ResourceRemovedEvent.create(id, resource.getName(), resource.getType(), resource.getUri(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId()));
-            cache.resourceRemoval(id, resource.getName(), resource.getType(), resource.getUri(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId(), invalidations);
+            invalidationEvents.add(ResourceRemovedEvent.create(id, resource.getName(), resource.getType(), resource.getUris(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId()));
+            cache.resourceRemoval(id, resource.getName(), resource.getType(), resource.getUris(), resource.getOwner(), resource.getScopes().stream().map(scope -> scope.getId()).collect(Collectors.toSet()), resource.getResourceServer().getId(), invalidations);
             getResourceStoreDelegate().delete(id);
 
         }
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
index 8b1d903..34653cc 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceEntity.java
@@ -21,7 +21,9 @@ package org.keycloak.authorization.jpa.entities;
 import javax.persistence.Access;
 import javax.persistence.AccessType;
 import javax.persistence.CascadeType;
+import javax.persistence.CollectionTable;
 import javax.persistence.Column;
+import javax.persistence.ElementCollection;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
 import javax.persistence.Id;
@@ -37,8 +39,10 @@ import javax.persistence.UniqueConstraint;
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import org.hibernate.annotations.Fetch;
 import org.hibernate.annotations.FetchMode;
@@ -54,7 +58,7 @@ import org.hibernate.annotations.FetchMode;
         {
                 @NamedQuery(name="findResourceIdByOwner", query="select r.id from ResourceEntity r where r.resourceServer.id = :serverId and r.owner = :owner"),
                 @NamedQuery(name="findAnyResourceIdByOwner", query="select r.id from ResourceEntity r where r.owner = :owner"),
-                @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where  r.resourceServer.id = :serverId  and r.uri = :uri"),
+                @NamedQuery(name="findResourceIdByUri", query="select r.id from ResourceEntity r where  r.resourceServer.id = :serverId  and :uri in elements(r.uris)"),
                 @NamedQuery(name="findResourceIdByName", query="select r.id from ResourceEntity r where  r.resourceServer.id = :serverId  and r.owner = :ownerId and r.name = :name"),
                 @NamedQuery(name="findResourceIdByType", query="select r.id from ResourceEntity r where  r.resourceServer.id = :serverId  and r.owner = :ownerId and r.type = :type"),
                 @NamedQuery(name="findResourceIdByServerId", query="select r.id from ResourceEntity r where  r.resourceServer.id = :serverId "),
@@ -75,8 +79,10 @@ public class ResourceEntity {
     @Column(name = "DISPLAY_NAME")
     private String displayName;
 
-    @Column(name = "URI")
-    private String uri;
+    @ElementCollection(fetch = FetchType.EAGER)
+    @Column(name = "VALUE")
+    @CollectionTable(name = "RESOURCE_URIS", joinColumns = { @JoinColumn(name="RESOURCE_ID") })
+    private Set<String> uris = new HashSet<>();
 
     @Column(name = "TYPE")
     private String type;
@@ -130,12 +136,12 @@ public class ResourceEntity {
         this.displayName = displayName;
     }
 
-    public String getUri() {
-        return uri;
+    public Set<String> getUris() {
+        return uris;
     }
 
-    public void setUri(String uri) {
-        this.uri = uri;
+    public void setUris(Set<String> uris) {
+        this.uris = uris;
     }
 
     public String getType() {
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
index 38ddb80..c39717e 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/JPAResourceStore.java
@@ -31,6 +31,7 @@ import javax.persistence.Query;
 import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Expression;
 import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
 import java.util.ArrayList;
@@ -192,9 +193,12 @@ public class JPAResourceStore implements ResourceStore {
             } else if ("ownerManagedAccess".equals(name)) {
                 predicates.add(builder.equal(root.get(name), Boolean.valueOf(value[0])));
             } else if ("uri".equals(name)) {
-                predicates.add(builder.equal(builder.lower(root.get(name)), value[0].toLowerCase()));
+                predicates.add(builder.lower(root.join("uris")).in(value[0].toLowerCase()));
             } else if ("uri_not_null".equals(name)) {
-                predicates.add(builder.isNotNull(root.get("uri")));
+                // predicates.add(builder.isNotEmpty(root.get("uris"))); looks like there is a bug in hibernate and this line doesn't work: https://hibernate.atlassian.net/browse/HHH-6686
+                // Workaround
+                Expression<Integer> urisSize = builder.size(root.get("uris"));
+                predicates.add(builder.notEqual(urisSize, 0));
             } else if ("owner".equals(name)) {
                 predicates.add(root.get(name).in(value));
             } else {
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
index 02320ec..64f66db 100644
--- a/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/store/ResourceAdapter.java
@@ -81,19 +81,18 @@ public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
     }
 
     @Override
-    public void setName(String name) {
-        entity.setName(name);
-
+    public Set<String> getUris() {
+        return entity.getUris();
     }
 
     @Override
-    public String getUri() {
-        return entity.getUri();
+    public void updateUris(Set<String> uri) {
+        entity.setUris(uri);
     }
 
     @Override
-    public void setUri(String uri) {
-        entity.setUri(uri);
+    public void setName(String name) {
+        entity.setName(name);
 
     }
 
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AuthzResourceUseMoreURIs.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AuthzResourceUseMoreURIs.java
new file mode 100644
index 0000000..03ad7e6
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/AuthzResourceUseMoreURIs.java
@@ -0,0 +1,49 @@
+package org.keycloak.connections.jpa.updater.liquibase.custom;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.statement.core.InsertStatement;
+import liquibase.structure.core.Table;
+
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+
+/**
+ * @author mhajas
+ */
+public class AuthzResourceUseMoreURIs extends CustomKeycloakTask {
+    @Override
+    protected void generateStatementsImpl() throws CustomChangeException {
+        try {
+            PreparedStatement statement = jdbcConnection.prepareStatement("select ID,URI from " + getTableName("RESOURCE_SERVER_RESOURCE"));
+
+            try {
+                ResultSet resultSet = statement.executeQuery();
+                try {
+                    while (resultSet.next()) {
+                        String resourceId = resultSet.getString(1);
+                        String resourceUri = resultSet.getString(2);
+
+                        InsertStatement insertComponent = new InsertStatement(null, null, database.correctObjectName("RESOURCE_URI", Table.class))
+                                .addColumnValue("RESOURCE_ID", resourceId)
+                                .addColumnValue("VALUE", resourceUri);
+
+                        statements.add(insertComponent);
+                    }
+                } finally {
+                    resultSet.close();
+                }
+            } finally {
+                statement.close();
+            }
+
+            confirmationMessage.append("Moved " + statements.size() + " records from RESOURCE_SERVER_RESOURCE to RESOURCE_URI table");
+        } catch (Exception e) {
+            throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
+        }
+    }
+
+    @Override
+    protected String getTaskId() {
+        return "Update 4.2.0.Final-SNAPSHOT";
+    }
+}
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.2.0.Final-SNAPSHOT.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.2.0.Final-SNAPSHOT.xml
new file mode 100644
index 0000000..c7729bf
--- /dev/null
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.2.0.Final-SNAPSHOT.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+  ~ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+  ~ * and other contributors as indicated by the @author tags.
+  ~ *
+  ~ * Licensed under the Apache License, Version 2.0 (the "License");
+  ~ * you may not use this file except in compliance with the License.
+  ~ * You may obtain a copy of the License at
+  ~ *
+  ~ * http://www.apache.org/licenses/LICENSE-2.0
+  ~ *
+  ~ * Unless required by applicable law or agreed to in writing, software
+  ~ * distributed under the License is distributed on an "AS IS" BASIS,
+  ~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ * See the License for the specific language governing permissions and
+  ~ * limitations under the License.
+  -->
+
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
+    <changeSet author="mhajas@redhat.com" id="authz-4.2.0.Final-SNAPSHOT">
+        <createTable tableName="RESOURCE_URIS">
+            <column name="RESOURCE_ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="VALUE" type="VARCHAR(255)">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+
+        <addForeignKeyConstraint baseColumnNames="RESOURCE_ID" baseTableName="RESOURCE_URIS" constraintName="FK_RESOURCE_SERVER_URIS" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_RESOURCE"/>
+
+        <customChange class="org.keycloak.connections.jpa.updater.liquibase.custom.AuthzResourceUseMoreURIs"/>
+
+        <dropColumn columnName="URI" tableName="RESOURCE_SERVER_RESOURCE"/>
+    </changeSet>
+</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
index 554df64..703e677 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-master.xml
@@ -56,4 +56,5 @@
     <include file="META-INF/jpa-changelog-4.0.0.xml"/>
     <include file="META-INF/jpa-changelog-authz-4.0.0.CR1.xml"/>
     <include file="META-INF/jpa-changelog-authz-4.0.0.Beta3.xml"/>
+    <include file="META-INF/jpa-changelog-authz-4.2.0.Final-SNAPSHOT.xml"/>
 </databaseChangeLog>
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
index dd7f5d7..2cf2ea1 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/model/Resource.java
@@ -65,18 +65,19 @@ public interface Resource {
     void setDisplayName(String name);
 
     /**
-     * Returns a {@link java.net.URI} that uniquely identify this resource.
+     * Returns a {@link List} containing all {@link java.net.URI} that uniquely identify this resource.
      *
-     * @return an {@link java.net.URI} for this resource or null if not defined.
+     * @return a {@link List} if {@link java.net.URI} for this resource or empty list if not defined.
      */
-    String getUri();
+    Set<String> getUris();
 
     /**
-     * Sets a {@link java.net.URI} that uniquely identify this resource.
+     * Sets a list of {@link java.net.URI} that uniquely identify this resource.
      *
      * @param uri an {@link java.net.URI} for this resource
      */
-    void setUri(String uri);
+    void updateUris(Set<String> uri);
+
 
     /**
      * Returns a string representing the type of this resource.
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 24abbe4..17e8c1e 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -38,11 +38,12 @@ import org.keycloak.provider.ProviderConfigProperty;
 import org.keycloak.representations.idm.*;
 import org.keycloak.representations.idm.authorization.*;
 import org.keycloak.storage.StorageId;
-import org.keycloak.util.JsonSerialization;
 
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -788,7 +789,7 @@ public class ModelToRepresentation {
         resource.setType(model.getType());
         resource.setName(model.getName());
         resource.setDisplayName(model.getDisplayName());
-        resource.setUri(model.getUri());
+        resource.setUris(model.getUris());
         resource.setIconUri(model.getIconUri());
         resource.setOwnerManagedAccess(model.isOwnerManagedAccess());
 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index 64e4719..9089fb2 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -2355,7 +2355,7 @@ public class RepresentationToModel {
             existing.setName(resource.getName());
             existing.setDisplayName(resource.getDisplayName());
             existing.setType(resource.getType());
-            existing.setUri(resource.getUri());
+            existing.updateUris(resource.getUris());
             existing.setIconUri(resource.getIconUri());
             existing.setOwnerManagedAccess(Boolean.TRUE.equals(resource.getOwnerManagedAccess()));
             existing.updateScopes(resource.getScopes().stream()
@@ -2387,7 +2387,7 @@ public class RepresentationToModel {
 
         model.setDisplayName(resource.getDisplayName());
         model.setType(resource.getType());
-        model.setUri(resource.getUri());
+        model.updateUris(resource.getUris());
         model.setIconUri(resource.getIconUri());
         model.setOwnerManagedAccess(Boolean.TRUE.equals(resource.getOwnerManagedAccess()));
 
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
index 9c7a291..85b7804 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceServerService.java
@@ -54,6 +54,8 @@ import org.keycloak.representations.idm.authorization.ResourceServerRepresentati
 import org.keycloak.services.resources.admin.AdminEventBuilder;
 import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
 
+import java.util.Collections;
+
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
  */
@@ -221,7 +223,7 @@ public class ResourceServerService {
         ResourceRepresentation defaultResource = new ResourceRepresentation();
 
         defaultResource.setName("Default Resource");
-        defaultResource.setUri("/*");
+        defaultResource.setUris(Collections.singleton("/*"));
         defaultResource.setType("urn:" + this.client.getClientId() + ":resources:default");
 
         getResourceSetResource().create(defaultResource);
diff --git a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
index 5e2ba95..3fded6b 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -21,7 +21,6 @@ import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
 import static org.keycloak.models.utils.RepresentationToModel.toModel;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -420,22 +419,28 @@ public class ResourceSetService {
             attributes.put("owner", new String[] {resourceServer.getId()});
 
             List<Resource> serverResources = storeFactory.getResourceStore().findByResourceServer(attributes, this.resourceServer.getId(), firstResult != null ? firstResult : -1, maxResult != null ? maxResult : Constants.DEFAULT_MAX_RESULTS);
-            PathMatcher<Resource> pathMatcher = new PathMatcher<Resource>() {
+
+            PathMatcher<Map.Entry<String, Resource>> pathMatcher = new PathMatcher<Map.Entry<String, Resource>>() {
                 @Override
-                protected String getPath(Resource entry) {
-                    return entry.getUri();
+                protected String getPath(Map.Entry<String, Resource> entry) {
+                    return entry.getKey();
                 }
 
                 @Override
-                protected Collection<Resource> getPaths() {
-                    return serverResources;
+                protected Collection<Map.Entry<String, Resource>> getPaths() {
+                    Map<String, Resource> result = new HashMap<>();
+                    serverResources.forEach(resource -> resource.getUris().forEach(uri -> {
+                        result.put(uri, resource);
+                    }));
+
+                    return result.entrySet();
                 }
             };
 
-            Resource matches = pathMatcher.matches(uri);
+            Map.Entry<String, Resource> matches = pathMatcher.matches(uri);
 
             if (matches != null) {
-                resources = Arrays.asList(matches);
+                resources = Collections.singletonList(matches.getValue());
             }
         }
 
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
index 302dd96..ca5d675 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/UmaResourceRepresentation.java
@@ -37,7 +37,7 @@ public class UmaResourceRepresentation extends ResourceRepresentation {
         setId(resource.getId());
         setName(resource.getName());
         setType(resource.getType());
-        setUri(resource.getUri());
+        setUris(resource.getUris());
         setIconUri(resource.getIconUri());
         setOwner(resource.getOwner());
         setScopes(resource.getScopes());
@@ -49,7 +49,7 @@ public class UmaResourceRepresentation extends ResourceRepresentation {
         setId(resource.getId());
         setName(resource.getName());
         setType(resource.getType());
-        setUri(resource.getUri());
+        setUris(resource.getUris());
         setIconUri(resource.getIconUri());
         setOwner(resource.getOwner());
         setScopes(resource.getScopes().stream().map(scope -> new ScopeRepresentation(scope.getName())).collect(Collectors.toSet()));
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
index b986bb6..1a1a4a7 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json
@@ -60,6 +60,10 @@
     {
       "name": "Resource Protected With Claim",
       "uri": "/protected/context/context.jsp"
+    },
+    {
+      "name": "Multiple URL resource",
+      "uris": ["/keycloak-7269/sub-resource1/*", "/keycloak-7269/sub-resource2/{whatever-pattern}/page.jsp"]
     }
   ],
   "policies": [
@@ -199,6 +203,16 @@
       }
     },
     {
+      "name": "Permission for multiple url resource",
+      "type": "resource",
+      "logic": "POSITIVE",
+      "decisionStrategy": "UNANIMOUS",
+      "config": {
+        "resources": "[\"Multiple URL resource\"]",
+        "applyPolicies": "[\"All Users Policy\"]"
+      }
+    },
+    {
       "name": "Request Claim Policy",
       "description": "A policy that grants access based on claims from an http request",
       "type": "js",
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index1.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index1.jsp
new file mode 100644
index 0000000..e23a3b2
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index1.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>sub-resource1 index1.jsp</h2>
+    <%@include file="../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index2.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index2.jsp
new file mode 100644
index 0000000..e24fde1
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource1/index2.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>sub-resource1 index2.jsp</h2>
+    <%@include file="../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern1/page.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern1/page.jsp
new file mode 100644
index 0000000..9d46c6a
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern1/page.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>sub-resource2/pattern1</h2>
+    <%@include file="../../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern2/page.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern2/page.jsp
new file mode 100644
index 0000000..76f2fb3
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/pattern2/page.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>sub-resource2/pattern2</h2>
+    <%@include file="../../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/test.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/test.jsp
new file mode 100644
index 0000000..6dc254a
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/sub-resource2/test.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>keycloak-7269/sub-resource2/test</h2>
+    <%@include file="../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/test.jsp b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/test.jsp
new file mode 100644
index 0000000..aa791e2
--- /dev/null
+++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/keycloak-7269/test.jsp
@@ -0,0 +1,6 @@
+<html>
+<body>
+    <h2>keycloak-7269/test</h2>
+    <%@include file="../../logout-include.jsp"%>
+</body>
+</html>
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json
index 9e984b5..d4c64aa 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/servlet-policy-enforcer-authz-realm.json
@@ -120,6 +120,10 @@
                         "name": "Pattern 15",
                         "type": "pattern-15",
                         "uri": "/keycloak-7148/{id}"
+                    },
+                    {
+                        "name": "Pattern 16",
+                        "uris": ["/keycloak-7269/sub-resource1", "/keycloak-7269/sub-resource2/*", "/keycloak-7269/sub-resource1/{test-pattern}/specialSuffix"]
                     }
                 ],
                 "policies": [
@@ -302,6 +306,16 @@
                             "default": "true",
                             "applyPolicies": "[\"Default Policy\"]"
                         }
+                    },
+                    {
+                        "name": "Pattern 16 Permission",
+                        "type": "resource",
+                        "logic": "POSITIVE",
+                        "decisionStrategy": "UNANIMOUS",
+                        "config": {
+                            "resources": "[\"Pattern 16\"]",
+                            "applyPolicies": "[\"Default Policy\"]"
+                        }
                     }
                 ],
                 "scopes": []
diff --git a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
index 43735e0..aeab19c 100644
--- a/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
+++ b/testsuite/integration-arquillian/test-apps/servlet-policy-enforcer/src/main/webapp/WEB-INF/keycloak.json
@@ -73,6 +73,18 @@
             {
                 "name": "Pattern 15",
                 "path": "/keycloak-7148/{id}/*"
+            },
+            {
+                "name": "Pattern 16",
+                "path": "/keycloak-7269/sub-resource1"
+            },
+            {
+                "name": "Pattern 16",
+                "path": "/keycloak-7269/sub-resource2/*"
+            },
+            {
+                "name": "Pattern 16",
+                "path": "/keycloak-7269/sub-resource1/{test-pattern}/specialSuffix"
             }
         ]
     }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
index b8f9773..da5f68b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/AbstractServletAuthzAdapterTest.java
@@ -31,6 +31,7 @@ import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation
 import org.keycloak.representations.idm.authorization.ResourceRepresentation;
 import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
 import org.keycloak.testsuite.util.WaitUtils;
+import org.openqa.selenium.By;
 
 import javax.ws.rs.core.Response;
 import java.util.Arrays;
@@ -38,7 +39,7 @@ import java.util.List;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
-import static org.keycloak.testsuite.adapter.example.authorization.AbstractBaseServletAuthzAdapterTest.RESOURCE_SERVER_ID;
+import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
 
 /**
  * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
@@ -309,4 +310,60 @@ public abstract class AbstractServletAuthzAdapterTest extends AbstractBaseServle
             assertTrue(hasText("Granted"));
         });
     }
+
+    @Test
+    public void testMultipleURLsForResourceRealmConfig() throws Exception {
+        performTests(() -> {
+            login("jdoe", "jdoe");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index1.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource1 index1.jsp");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index2.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource1 index2.jsp");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern1/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource2/pattern1");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern2/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource2/pattern2");
+
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/test");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/sub-resource2/test");
+
+            updatePermissionPolicies("Permission for multiple url resource", "Deny Policy");
+            login("jdoe", "jdoe");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index1.jsp");
+            waitUntilElement(By.tagName("h2")).text().not().contains("sub-resource1 index1.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("You can not access this resource.");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index2.jsp");
+            waitUntilElement(By.tagName("h2")).text().not().contains("sub-resource1 index2.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("You can not access this resource.");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern1/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().not().contains("sub-resource2/pattern1");
+            waitUntilElement(By.tagName("h2")).text().contains("You can not access this resource.");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern2/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().not().contains("sub-resource2/pattern2");
+            waitUntilElement(By.tagName("h2")).text().contains("You can not access this resource.");
+
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/test");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/sub-resource2/test");
+
+            updatePermissionPolicies("Permission for multiple url resource", "All Users Policy");
+            login("jdoe", "jdoe");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index1.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource1 index1.jsp");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource1/index2.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource1 index2.jsp");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern1/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource2/pattern1");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/pattern2/page.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("sub-resource2/pattern2");
+
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/test");
+            driver.navigate().to(getResourceServerUrl() + "/keycloak-7269/sub-resource2/test.jsp");
+            waitUntilElement(By.tagName("h2")).text().contains("keycloak-7269/sub-resource2/test");
+        });
+    }
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java
index 76e11d6..97df88b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletPolicyEnforcerTest.java
@@ -451,6 +451,43 @@ public class ServletPolicyEnforcerTest extends AbstractExampleAdapterTest {
         });
     }
 
+    @Test
+    public void testMultipleUriForResourceJSONConfig() {
+        performTests(() -> {
+            login("alice", "alice");
+            navigateTo("/keycloak-7269/sub-resource1");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource1/whatever/specialSuffix");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2/w/h/a/t/e/v/e/r");
+            assertFalse(wasDenied());
+
+            updatePermissionPolicies("Pattern 16 Permission", "Deny Policy");
+
+            login("alice", "alice");
+            navigateTo("/keycloak-7269/sub-resource1");
+            assertTrue(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource1/whatever/specialSuffix");
+            assertTrue(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2");
+            assertTrue(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2/w/h/a/t/e/v/e/r");
+            assertTrue(wasDenied());
+
+            updatePermissionPolicies("Pattern 16 Permission", "Default Policy");
+            navigateTo("/keycloak-7269/sub-resource1");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource1/whatever/specialSuffix");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2");
+            assertFalse(wasDenied());
+            navigateTo("/keycloak-7269/sub-resource2/w/h/a/t/e/v/e/r");
+            assertFalse(wasDenied());
+        });
+    }
+
     private void navigateTo(String path) {
         this.driver.navigate().to(getResourceServerUrl() + path);
     }
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
index 59b2177..0489116 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/resource/ResourceForm.java
@@ -28,6 +28,7 @@ import org.keycloak.testsuite.page.Form;
 import org.keycloak.testsuite.util.WaitUtils;
 import org.openqa.selenium.By;
 import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.NoSuchElementException;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
 import org.openqa.selenium.support.FindBy;
@@ -46,8 +47,11 @@ public class ResourceForm extends Form {
     @FindBy(id = "type")
     private WebElement type;
 
-    @FindBy(id = "uri")
-    private WebElement uri;
+    @FindBy(id = "newUri")
+    private WebElement newUri;
+
+    @FindBy(xpath = "//*[@id=\"view\"]/div[1]/form/fieldset/div[5]/div/div/div/button")
+    private WebElement addUriButton;
 
     @FindBy(id = "iconUri")
     private WebElement iconUri;
@@ -65,10 +69,24 @@ public class ResourceForm extends Form {
     private ScopesInput scopesInput;
 
     public void populate(ResourceRepresentation expected) {
+        while (true) {
+            try {
+                WebElement e = driver.findElement(By.xpath("//button[@data-ng-click='deleteUri($index)']"));
+                e.click();
+            } catch (NoSuchElementException e) {
+                break;
+            }
+        }
+
         setInputValue(name, expected.getName());
         setInputValue(displayName, expected.getDisplayName());
         setInputValue(type, expected.getType());
-        setInputValue(uri, expected.getUri());
+
+        for (String uri : expected.getUris()) {
+            setInputValue(newUri, uri);
+            addUriButton.click();
+        }
+
         setInputValue(iconUri, expected.getIconUri());
 
         Set<ScopeRepresentation> scopes = expected.getScopes();
@@ -108,7 +126,11 @@ public class ResourceForm extends Form {
         representation.setName(getInputValue(name));
         representation.setDisplayName(getInputValue(displayName));
         representation.setType(getInputValue(type));
-        representation.setUri(getInputValue(uri));
+
+        for (WebElement uriInput : driver.findElements(By.xpath("//input[@ng-model='resource.uris[i]']"))) {
+            representation.addUri(getInputValue(uriInput));
+        }
+
         representation.setIconUri(getInputValue(iconUri));
         representation.setScopes(scopesInput.getSelected());
 
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 5dc7e70..39731b3 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1201,6 +1201,7 @@ error=Error
 authz-authorization=Authorization
 authz-owner=Owner
 authz-uri=URI
+authz-uris=URIS
 authz-scopes=Scopes
 authz-resource=Resource
 authz-resource-type=Resource Type
@@ -1257,7 +1258,7 @@ authz-add-resource=Add Resource
 authz-resource-name.tooltip=A unique name for this resource. The name can be used to uniquely identify a resource, useful when querying for a specific resource.
 authz-resource-owner.tooltip=The owner of this resource.
 authz-resource-type.tooltip=The type of this resource. It can be used to group different resource instances with the same type.
-authz-resource-uri.tooltip=An URI that can also be used to uniquely identify this resource.
+authz-resource-uri.tooltip=Set of URIs which are protected by resource.
 authz-resource-scopes.tooltip=The scopes associated with this resource.
 authz-resource-attributes=Resource Attributes
 authz-resource-attributes.tooltip=The attributes associated wth the resource.
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 6fab666..5dbf2a9 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -301,6 +301,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
             var resource = {};
             resource.scopes = [];
             resource.attributes = {};
+            resource.uris = [];
 
             $scope.resource = angular.copy(resource);
 
@@ -310,7 +311,17 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
                 }
             }, true);
 
+            $scope.$watch('newUri', function() {
+                if ($scope.newUri && $scope.newUri.length > 0) {
+                    $scope.changed = true;
+                }
+            }, true);
+
             $scope.save = function() {
+                if ($scope.newUri && $scope.newUri.length > 0) {
+                    $scope.addUri();
+                }
+
                 for (i = 0; i < $scope.resource.scopes.length; i++) {
                     delete $scope.resource.scopes[i].text;
                 }
@@ -350,7 +361,17 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
                     }
                 }, true);
 
+                $scope.$watch('newUri', function() {
+                    if ($scope.newUri && $scope.newUri.length > 0) {
+                        $scope.changed = true;
+                    }
+                }, true);
+
                 $scope.save = function() {
+                    if ($scope.newUri && $scope.newUri.length > 0) {
+                        $scope.addUri();
+                    }
+
                     for (i = 0; i < $scope.resource.scopes.length; i++) {
                         delete $scope.resource.scopes[i].text;
                     }
@@ -412,6 +433,15 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
     $scope.removeAttribute = function(key) {
         delete $scope.resource.attributes[key];
     }
+
+    $scope.addUri = function() {
+        $scope.resource.uris.push($scope.newUri);
+        $scope.newUri = "";
+    }
+
+    $scope.deleteUri = function(index) {
+        $scope.resource.uris.splice(index, 1);
+    }
 });
 
 var Scopes = {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
index 2311f1b..b3d6eca 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-detail.html
@@ -44,9 +44,21 @@
                 <kc-tooltip>{{:: 'authz-resource-type.tooltip' | translate}}</kc-tooltip>
             </div>
             <div class="form-group">
-                <label class="col-md-2 control-label" for="uri">{{:: 'authz-uri' | translate}} </label>
+                <label class="col-md-2 control-label" for="newUri">{{:: 'authz-uri' | translate}} </label>
                 <div class="col-sm-6">
-                    <input class="form-control" type="text" id="uri" name="name" data-ng-model="resource.uri" autofocus>
+                    <div class="input-group" ng-repeat="(i, uri) in resource.uris track by $index">
+                        <input class="form-control" ng-model="resource.uris[i]">
+                        <div class="input-group-btn">
+                            <button class="btn btn-default" type="button" data-ng-click="deleteUri($index)"><span class="fa fa-minus"></span></button>
+                        </div>
+                    </div>
+
+                    <div class="input-group">
+                        <input class="form-control" ng-model="newUri" id="newUri">
+                        <div class="input-group-btn">
+                            <button class="btn btn-default" type="button" data-ng-click="newUri.length > 0 && addUri()"><span class="fa fa-plus"></span></button>
+                        </div>
+                    </div>
                 </div>
                 <kc-tooltip>{{:: 'authz-resource-uri.tooltip' | translate}}</kc-tooltip>
             </div>
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
index c9152c0..dce000e 100644
--- a/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/resource-server-resource-list.html
@@ -72,7 +72,7 @@
             <th width="1%"></th>
             <th>{{:: 'name' | translate}}</th>
             <th>{{:: 'type' | translate}}</th>
-            <th>{{:: 'authz-uri' | translate}}</th>
+            <th>{{:: 'authz-uris' | translate}}</th>
             <th>{{:: 'authz-owner' | translate}}</th>
             <th width="11%" style="text-align: center">{{:: 'actions' | translate}}</th>
         </tr>
@@ -108,8 +108,9 @@
                 <span data-ng-show="!resource.type">{{:: 'authz-no-type-defined' | translate}}</span>
             </td>
             <td>
-                <span data-ng-show="resource.uri">{{resource.uri}}</span>
-                <span data-ng-show="!resource.uri">{{:: 'authz-no-uri-defined' | translate}}</span>
+                <span data-ng-show="resource.uris.length == 0">{{:: 'authz-no-uri-defined' | translate}}</span>
+                <span data-ng-show="resource.uris.length == 1">{{resource.uris[0]}}</span>
+                <span data-ng-show="resource.uris.length > 1">{{resource.uris.length}} {{:: 'authz-uris' | translate}}</span>
             </td>
             <td>{{resource.owner.name}}</td>
             <td align="center">
@@ -144,6 +145,11 @@
                                     <span data-ng-show="resource.policies && !resource.policies.length">{{:: 'authz-no-permission-assigned' | translate}}</span>
                                     <span ng-repeat="policy in resource.policies" data-ng-show="resource.policies.length > 0"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission/{{policy.type}}/{{policy.id}}">{{policy.name}}</a>{{$last ? '' : ', '}}</span>
                                 </dd>
+                                <dt>{{:: 'authz-uris' | translate}}</dt>
+                                <dd>
+                                    <span data-ng-show="resource.uris && !resource.uris.length">{{:: 'authz-no-uri-defined' | translate}}</span>
+                                    <span ng-repeat="uri in resource.uris" data-ng-show="resource.uris.length > 0">{{uri}}{{$last ? '' : ', '}}</span>
+                                </dd>
                             </dl>
                         </div>
                     </div>