keycloak-uncached
Changes
authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java 11(+11 -0)
core/src/main/java/org/keycloak/representations/idm/authorization/ResourceRepresentation.java 15(+15 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/entities/CachedResource.java 9(+9 -0)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/authorization/ResourceAdapter.java 46(+46 -0)
model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceAttributeEntity.java 104(+104 -0)
services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java 12(+12 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java 39(+39 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java 4(+4 -0)
Details
diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
index c2e8f8a..fa2622e 100644
--- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/ResourceRepresentation.java
@@ -20,6 +20,8 @@ package org.keycloak.authorization.client.representation;
import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -49,6 +51,7 @@ public class ResourceRepresentation {
private String iconUri;
private String owner;
private Boolean ownerManagedAccess;
+ private Map<String, List<String>> attributes;
/**
* Creates a new instance.
@@ -204,4 +207,12 @@ public class ResourceRepresentation {
", scopes=" + scopes +
'}';
}
+
+ public void setAttributes(Map<String, List<String>> attributes) {
+ this.attributes = attributes;
+ }
+
+ public Map<String, List<String>> getAttributes() {
+ return attributes;
+ }
}
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 ce823c9..071bc32 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
@@ -20,11 +20,14 @@ import java.net.URI;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import org.keycloak.json.StringListMapDeserializer;
/**
* <p>One or more resources that the resource server manages as a set of protected resources.
@@ -53,6 +56,10 @@ public class ResourceRepresentation {
private List<PolicyRepresentation> policies;
private String displayName;
+
+ @JsonDeserialize(using = StringListMapDeserializer.class)
+ private Map<String, List<String>> attributes;
+
/**
* Creates a new instance.
*
@@ -195,6 +202,14 @@ public class ResourceRepresentation {
}
}
+ public Map<String, List<String>> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map<String, List<String>> attributes) {
+ this.attributes = attributes;
+ }
+
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
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 383ab1c..7e75410 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
@@ -20,8 +20,11 @@ package org.keycloak.models.cache.infinispan.authorization.entities;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.Scope;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -39,6 +42,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
private String uri;
private Set<String> scopesIds;
private boolean ownerManagedAccess;
+ private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
public CachedResource(Long revision, Resource resource) {
super(revision, resource.getId());
@@ -51,6 +55,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
this.resourceServerId = resource.getResourceServer().getId();
this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet());
ownerManagedAccess = resource.isOwnerManagedAccess();
+ this.attributes.putAll(resource.getAttributes());
}
@@ -89,4 +94,8 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
public Set<String> getScopesIds() {
return this.scopesIds;
}
+
+ public Map<String, List<String>> getAttributes() {
+ return attributes;
+ }
}
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 d310fca..4f59cdb 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
@@ -27,6 +27,7 @@ import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourc
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@@ -35,6 +36,7 @@ import java.util.stream.Collectors;
* @version $Revision: 1 $
*/
public class ResourceAdapter implements Resource, CachedModel<Resource> {
+
protected CachedResource cached;
protected StoreFactoryCacheSession cacheSession;
protected Resource updated;
@@ -211,6 +213,50 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
}
@Override
+ public Map<String, List<String>> getAttributes() {
+ if (updated != null) return updated.getAttributes();
+ return cached.getAttributes();
+ }
+
+ @Override
+ public String getSingleAttribute(String name) {
+ if (updated != null) return updated.getSingleAttribute(name);
+
+ List<String> values = cached.getAttributes().getOrDefault(name, Collections.emptyList());
+
+ if (values.isEmpty()) {
+ return null;
+ }
+
+ return values.get(0);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ if (updated != null) return updated.getAttribute(name);
+
+ List<String> values = cached.getAttributes().getOrDefault(name, Collections.emptyList());
+
+ if (values.isEmpty()) {
+ return null;
+ }
+
+ return Collections.unmodifiableList(values);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ getDelegateForUpdate();
+ updated.setAttribute(name, values);
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ getDelegateForUpdate();
+ updated.removeAttribute(name);
+ }
+
+ @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof Resource)) return false;
diff --git a/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceAttributeEntity.java b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceAttributeEntity.java
new file mode 100644
index 0000000..19963dc
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/authorization/jpa/entities/ResourceAttributeEntity.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018 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.
+ */
+package org.keycloak.authorization.jpa.entities;
+
+import javax.persistence.Access;
+import javax.persistence.AccessType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+@NamedQueries({
+ @NamedQuery(name="deleteResourceAttributesByNameAndResource", query="delete from ResourceAttributeEntity attr where attr.resource.id = :resourceId and attr.name = :name")
+})
+@Table(name="RESOURCE_ATTRIBUTE")
+@Entity
+public class ResourceAttributeEntity {
+
+ @Id
+ @Column(name="ID", length = 36)
+ @Access(AccessType.PROPERTY) // we do this because relationships often fetch id, but not entity. This avoids an extra SQL
+ private String id;
+
+ @ManyToOne(fetch= FetchType.LAZY)
+ @JoinColumn(name = "RESOURCE_ID")
+ private ResourceEntity resource;
+
+ @Column(name = "NAME")
+ private String name;
+ @Column(name = "VALUE")
+ private String value;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public ResourceEntity getResource() {
+ return resource;
+ }
+
+ public void setResource(ResourceEntity resource) {
+ this.resource = resource;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null) return false;
+ if (!(o instanceof ResourceAttributeEntity)) return false;
+
+ ResourceAttributeEntity that = (ResourceAttributeEntity) o;
+
+ if (!id.equals(that.getId())) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return id.hashCode();
+ }
+}
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 861853a..8c9960b 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
@@ -20,6 +20,7 @@ package org.keycloak.authorization.jpa.entities;
import javax.persistence.Access;
import javax.persistence.AccessType;
+import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
@@ -30,11 +31,18 @@ import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
+
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
+import org.hibernate.annotations.Fetch;
+import org.hibernate.annotations.FetchMode;
+
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@@ -94,6 +102,10 @@ public class ResourceEntity {
@JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID"))
private List<PolicyEntity> policies = new LinkedList<>();
+ @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="resource")
+ @Fetch(FetchMode.SUBSELECT)
+ private Collection<ResourceAttributeEntity> attributes = new ArrayList<>();
+
public String getId() {
return id;
}
@@ -179,6 +191,14 @@ public class ResourceEntity {
this.policies = policies;
}
+ public Collection<ResourceAttributeEntity> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Collection<ResourceAttributeEntity> attributes) {
+ this.attributes = attributes;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) return true;
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 782f084..02320ec 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
@@ -16,20 +16,27 @@
*/
package org.keycloak.authorization.jpa.store;
+import org.keycloak.authorization.jpa.entities.ResourceAttributeEntity;
import org.keycloak.authorization.jpa.entities.ResourceEntity;
import org.keycloak.authorization.jpa.entities.ScopeEntity;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.StoreFactory;
+import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.jpa.JpaModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager;
+import javax.persistence.Query;
+
+import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -37,6 +44,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
+
private ResourceEntity entity;
private EntityManager em;
private StoreFactory storeFactory;
@@ -158,6 +166,72 @@ public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
}
}
+ @Override
+ public Map<String, List<String>> getAttributes() {
+ MultivaluedHashMap<String, String> result = new MultivaluedHashMap<>();
+ for (ResourceAttributeEntity attr : entity.getAttributes()) {
+ result.add(attr.getName(), attr.getValue());
+ }
+ return result;
+ }
+
+ @Override
+ public String getSingleAttribute(String name) {
+ List<String> values = getAttributes().getOrDefault(name, Collections.emptyList());
+
+ if (values.isEmpty()) {
+ return null;
+ }
+
+ return values.get(0);
+ }
+
+ @Override
+ public List<String> getAttribute(String name) {
+ List<String> values = getAttributes().getOrDefault(name, Collections.emptyList());
+
+ if (values.isEmpty()) {
+ return null;
+ }
+
+ return Collections.unmodifiableList(values);
+ }
+
+ @Override
+ public void setAttribute(String name, List<String> values) {
+ removeAttribute(name);
+
+ for (String value : values) {
+ ResourceAttributeEntity attr = new ResourceAttributeEntity();
+ attr.setId(KeycloakModelUtils.generateId());
+ attr.setName(name);
+ attr.setValue(value);
+ attr.setResource(entity);
+ em.persist(attr);
+ entity.getAttributes().add(attr);
+ }
+ }
+
+ @Override
+ public void removeAttribute(String name) {
+ Query query = em.createNamedQuery("deleteResourceAttributesByNameAndResource");
+
+ query.setParameter("name", name);
+ query.setParameter("resourceId", entity.getId());
+
+ query.executeUpdate();
+
+ List<ResourceAttributeEntity> toRemove = new ArrayList<>();
+
+ for (ResourceAttributeEntity attr : entity.getAttributes()) {
+ if (attr.getName().equals(name)) {
+ toRemove.add(attr);
+ }
+ }
+
+ entity.getAttributes().removeAll(toRemove);
+ }
+
public static ResourceEntity toEntity(EntityManager em, Resource resource) {
if (resource instanceof ResourceAdapter) {
diff --git a/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml
index 1b72a34..6666052 100755
--- a/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml
+++ b/model/jpa/src/main/resources/META-INF/jpa-changelog-authz-4.0.0.CR1.xml
@@ -74,5 +74,21 @@
<constraints nullable="false"/>
</column>
</addColumn>
+
+ <createTable tableName="RESOURCE_ATTRIBUTE">
+ <column name="ID" type="VARCHAR(36)" defaultValue="sybase-needs-something-here">
+ <constraints nullable="false"/>
+ </column>
+ <column name="NAME" type="VARCHAR(255)">
+ <constraints nullable="false"/>
+ </column>
+ <column name="VALUE" type="VARCHAR(255)"/>
+ <column name="RESOURCE_ID" type="VARCHAR(36)">
+ <constraints nullable="false"/>
+ </column>
+ </createTable>
+
+ <addPrimaryKey columnNames="ID" constraintName="RES_ATTR_PK" tableName="RESOURCE_ATTRIBUTE"/>
+ <addForeignKeyConstraint baseColumnNames="RESOURCE_ID" baseTableName="RESOURCE_ATTRIBUTE" constraintName="FK_5HRM2VLF9QL5FU022KQEPOVBR" referencedColumnNames="ID" referencedTableName="RESOURCE_SERVER_RESOURCE"/>
</changeSet>
</databaseChangeLog>
diff --git a/model/jpa/src/main/resources/META-INF/persistence.xml b/model/jpa/src/main/resources/META-INF/persistence.xml
index 86eda78..805b7c8 100755
--- a/model/jpa/src/main/resources/META-INF/persistence.xml
+++ b/model/jpa/src/main/resources/META-INF/persistence.xml
@@ -67,6 +67,7 @@
<class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>
<class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>
<class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class>
+ <class>org.keycloak.authorization.jpa.entities.ResourceAttributeEntity</class>
<!-- User Federation Storage -->
<class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class>
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 cdfc0b6..dd7f5d7 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
@@ -19,6 +19,7 @@
package org.keycloak.authorization.model;
import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -126,8 +127,56 @@ public interface Resource {
*/
String getOwner();
+ /**
+ * Indicates if this resource can be managed by the resource owner.
+ *
+ * @return {@code true} if this resource can be managed by the resource owner. Otherwise, {@code false}.
+ */
boolean isOwnerManagedAccess();
+
+ /**
+ * Sets if this resource can be managed by the resource owner.
+ *
+ * @param ownerManagedAccess {@code true} indicates that this resource can be managed by the resource owner.
+ */
void setOwnerManagedAccess(boolean ownerManagedAccess);
+ /**
+ * Update the set of scopes associated with this resource.
+ *
+ * @param scopes the list of scopes to update
+ */
void updateScopes(Set<Scope> scopes);
+
+ /**
+ * Returns the attributes associated with this resource.
+ *
+ * @return a map holding the attributes associated with this resource
+ */
+ Map<String, List<String>> getAttributes();
+
+ /**
+ * Returns the first value of an attribute with the given <code>name</code>
+ *
+ * @return the first value of an attribute
+ */
+ String getSingleAttribute(String name);
+
+ /**
+ * Returns the values of an attribute with the given <code>name</code>
+ *
+ * @return the values of an attribute
+ */
+ List<String> getAttribute(String name);
+
+ /**
+ * Sets an attribute with the given <code>name</code> and <code>values</code>.
+ *
+ * @param name the attribute name
+ * @param value the attribute values
+ * @return a map holding the attributes associated with this resource
+ */
+ void setAttribute(String name, List<String> values);
+
+ void removeAttribute(String name);
}
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 22b4a3e..e793cb2 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
@@ -841,6 +841,8 @@ public class ModelToRepresentation {
}
return scope;
}).collect(Collectors.toSet()));
+
+ resource.setAttributes(new HashMap<>(model.getAttributes()));
}
return resource;
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 7324913..538dcef 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
@@ -26,6 +26,7 @@ import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -2351,6 +2352,24 @@ public class RepresentationToModel {
existing.updateScopes(resource.getScopes().stream()
.map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization))
.collect(Collectors.toSet()));
+ Map<String, List<String>> attributes = resource.getAttributes();
+
+ if (attributes != null) {
+ Set<String> existingAttrNames = existing.getAttributes().keySet();
+
+ for (String name : existingAttrNames) {
+ if (attributes.containsKey(name)) {
+ existing.setAttribute(name, attributes.get(name));
+ attributes.remove(name);
+ } else {
+ existing.removeAttribute(name);
+ }
+ }
+
+ for (String name : attributes.keySet()) {
+ existing.setAttribute(name, attributes.get(name));
+ }
+ }
return existing;
}
@@ -2369,6 +2388,14 @@ public class RepresentationToModel {
model.updateScopes(scopes.stream().map((Function<ScopeRepresentation, Scope>) scope -> toModel(scope, resourceServer, authorization)).collect(Collectors.toSet()));
}
+ Map<String, List<String>> attributes = resource.getAttributes();
+
+ if (attributes != null) {
+ for (Entry<String, List<String>> entry : attributes.entrySet()) {
+ model.setAttribute(entry.getKey(), entry.getValue());
+ }
+ }
+
resource.setId(model.getId());
return model;
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 04c9d74..b69d991 100644
--- a/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
+++ b/services/src/main/java/org/keycloak/authorization/admin/ResourceSetService.java
@@ -294,6 +294,22 @@ public class ResourceSetService {
return Response.ok(representation).build();
}
+ @Path("{id}/attributes")
+ @GET
+ @NoCache
+ @Produces("application/json")
+ public Response getAttributes(@PathParam("id") String id) {
+ requireView();
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ Resource model = storeFactory.getResourceStore().findById(id, resourceServer.getId());
+
+ if (model == null) {
+ return Response.status(Status.NOT_FOUND).build();
+ }
+
+ return Response.ok(model.getAttributes()).build();
+ }
+
@Path("/search")
@GET
@NoCache
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
index fbbe08e..1ac4608 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/representation/UmaResourceRepresentation.java
@@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URI;
import java.util.Collections;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
/**
@@ -49,6 +51,8 @@ public class UmaResourceRepresentation {
private String owner;
private Boolean ownerManagedAccess;
+ private Map<String, List<String>> attributes;
+
/**
* Creates a new instance.
@@ -161,4 +165,12 @@ public class UmaResourceRepresentation {
public Boolean getOwnerManagedAccess() {
return ownerManagedAccess;
}
+
+ public Map<String, List<String>> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map<String, List<String>> attributes) {
+ this.attributes = attributes;
+ }
}
diff --git a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
index 0cead7d..21fd27c 100644
--- a/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
+++ b/services/src/main/java/org/keycloak/authorization/protection/resource/ResourceService.java
@@ -150,6 +150,8 @@ public class ResourceService {
return scopeRepresentation;
}).collect(Collectors.toSet()));
+ resource.setAttributes(umaResource.getAttributes());
+
return resource;
}
@@ -178,6 +180,8 @@ public class ResourceService {
return umaScopeRep;
}).collect(Collectors.toSet()));
+ resource.setAttributes(model.getAttributes());
+
return resource;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
index 41fcb66..5b8384a 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementTest.java
@@ -28,8 +28,12 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
+
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@@ -53,6 +57,17 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
assertEquals("/test/*", newResource.getUri());
assertEquals("test-resource", newResource.getType());
assertEquals("icon-test-resource", newResource.getIconUri());
+
+ Map<String, List<String>> attributes = newResource.getAttributes();
+
+ assertEquals(2, attributes.size());
+
+ assertTrue(attributes.containsKey("a"));
+ assertTrue(attributes.containsKey("b"));
+ assertTrue(attributes.get("a").containsAll(Arrays.asList("a1", "a2", "a3")));
+ assertEquals(3, attributes.get("a").size());
+ assertTrue(attributes.get("b").containsAll(Arrays.asList("b1")));
+ assertEquals(1, attributes.get("b").size());
}
@Test
@@ -105,11 +120,28 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
resource.setIconUri("changed");
resource.setUri("changed");
+ Map<String, List<String>> attributes = resource.getAttributes();
+
+ attributes.remove("a");
+ attributes.put("c", Arrays.asList("c1", "c2"));
+ attributes.put("b", Arrays.asList("changed"));
+
resource = doUpdateResource(resource);
assertEquals("changed", resource.getIconUri());
assertEquals("changed", resource.getType());
assertEquals("changed", resource.getUri());
+
+ attributes = resource.getAttributes();
+
+ assertEquals(2, attributes.size());
+
+ assertFalse(attributes.containsKey("a"));
+ assertTrue(attributes.containsKey("b"));
+ assertTrue(attributes.get("b").containsAll(Arrays.asList("changed")));
+ assertEquals(1, attributes.get("b").size());
+ assertTrue(attributes.get("c").containsAll(Arrays.asList("c1", "c2")));
+ assertEquals(2, attributes.get("c").size());
}
@Test(expected = NotFoundException.class)
@@ -205,6 +237,13 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
newResource.setIconUri(iconUri);
newResource.setOwner(owner != null ? new ResourceOwnerRepresentation(owner) : null);
+ Map<String, List<String>> attributes = new HashMap<>();
+
+ attributes.put("a", Arrays.asList("a1", "a2", "a3"));
+ attributes.put("b", Arrays.asList("b1"));
+
+ newResource.setAttributes(attributes);
+
return doCreateResource(newResource);
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
index 536d122..32865ec 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourceManagementWithAuthzClientTest.java
@@ -83,6 +83,8 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
return scope;
}).collect(Collectors.toSet()));
+ resourceRepresentation.setAttributes(created.getAttributes());
+
return resourceRepresentation;
}
@@ -108,6 +110,8 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
return scope;
}).collect(Collectors.toSet()));
+ resource.setAttributes(newResource.getAttributes());
+
return resource;
}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java
index 9ffad7b..64431fe 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PolicyEvaluationTest.java
@@ -35,6 +35,7 @@ import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.common.DefaultEvaluationContext;
import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
@@ -556,9 +557,49 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
Assert.assertEquals(Effect.PERMIT, evaluation.getEffect());
}
- @NotNull
+ @Test
+ public void testCheckResourceAttributes() {
+ testingClient.server().run(PolicyEvaluationTest::testCheckResourceAttributes);
+ }
+
+ public static void testCheckResourceAttributes(KeycloakSession session) {
+ session.getContext().setRealm(session.realms().getRealmByName("authz-test"));
+ AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
+ ClientModel clientModel = session.realms().getClientByClientId("resource-server-test", session.getContext().getRealm());
+ StoreFactory storeFactory = authorization.getStoreFactory();
+ ResourceServer resourceServer = storeFactory.getResourceServerStore().findById(clientModel.getId());
+ JSPolicyRepresentation policyRepresentation = new JSPolicyRepresentation();
+
+ policyRepresentation.setName("testCheckResourceAttributes");
+ StringBuilder builder = new StringBuilder();
+
+ builder.append("var permission = $evaluation.getPermission();");
+ builder.append("var resource = permission.getResource();");
+ builder.append("var attributes = resource.getAttributes();");
+ builder.append("if (attributes.size() == 2 && attributes.containsKey('a1') && attributes.containsKey('a2') && attributes.get('a1').size() == 2 && attributes.get('a2').get(0).equals('3') && resource.getAttribute('a1').size() == 2 && resource.getSingleAttribute('a2').equals('3')) { $evaluation.grant(); }");
+
+ policyRepresentation.setCode(builder.toString());
+
+ Policy policy = storeFactory.getPolicyStore().create(policyRepresentation, resourceServer);
+ PolicyProvider provider = authorization.getProvider(policy.getType());
+ Resource resource = storeFactory.getResourceStore().create("testCheckResourceAttributesResource", resourceServer, resourceServer.getId());
+
+ resource.setAttribute("a1", Arrays.asList("1", "2"));
+ resource.setAttribute("a2", Arrays.asList("3"));
+
+ DefaultEvaluation evaluation = createEvaluation(session, authorization, resource, resourceServer, policy);
+
+ provider.evaluate(evaluation);
+
+ Assert.assertEquals(Effect.PERMIT, evaluation.getEffect());
+ }
+
private static DefaultEvaluation createEvaluation(KeycloakSession session, AuthorizationProvider authorization, ResourceServer resourceServer, Policy policy) {
- return new DefaultEvaluation(new ResourcePermission(null, null, resourceServer), new DefaultEvaluationContext(new Identity() {
+ return createEvaluation(session, authorization, null, resourceServer, policy);
+ }
+
+ private static DefaultEvaluation createEvaluation(KeycloakSession session, AuthorizationProvider authorization, Resource resource, ResourceServer resourceServer, Policy policy) {
+ return new DefaultEvaluation(new ResourcePermission(resource, null, resourceServer), new DefaultEvaluationContext(new Identity() {
@Override
public String getId() {
return null;
@@ -568,11 +609,8 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
public Attributes getAttributes() {
return null;
}
- }, session), policy, policy, new Decision() {
- @Override
- public void onDecision(Evaluation evaluation) {
+ }, session), policy, policy, evaluation -> {
- }
}, authorization);
}
}
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 12c5e05..690b88f 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
@@ -1183,6 +1183,10 @@ 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-scopes.tooltip=The scopes associated with this resource.
+authz-resource-attributes=Resource Attributes
+authz-resource-attributes.tooltip=The attributes associated wth the resource.
+authz-resource-user-managed-access-enabled=User-Managed Access Enabled
+authz-resource-user-managed-access-enabled.tooltip=If enabled this access to this resource can be managed by the resource owner.
# Authz Scope List
authz-add-scope=Add Scope
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 612fa97..8636410 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
@@ -294,6 +294,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
var resource = {};
resource.scopes = [];
+ resource.attributes = {};
$scope.resource = angular.copy(resource);
@@ -328,6 +329,10 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
data.scopes = [];
}
+ if (!data.attributes) {
+ data.attributes = {};
+ }
+
$scope.resource = angular.copy(data);
$scope.changed = false;
@@ -343,6 +348,15 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
for (i = 0; i < $scope.resource.scopes.length; i++) {
delete $scope.resource.scopes[i].text;
}
+ for (var [key, value] of Object.entries($scope.resource.attributes)) {
+ var values = value.toString().split(',');
+
+ $scope.resource.attributes[key] = [];
+
+ for (j = 0; j < values.length; j++) {
+ $scope.resource.attributes[key].push(values[j]);
+ }
+ }
$instance.checkNameAvailability(function () {
ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() {
$route.reload();
@@ -383,6 +397,15 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
}
});
}
+
+ $scope.addAttribute = function() {
+ $scope.resource.attributes[$scope.newAttribute.key] = $scope.newAttribute.value;
+ delete $scope.newAttribute;
+ }
+
+ $scope.removeAttribute = function(key) {
+ delete $scope.resource.attributes[key];
+ }
});
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 c68b0e2..2311f1b 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
@@ -67,11 +67,38 @@
<kc-tooltip>{{:: 'authz-icon-uri.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group">
- <label class="col-md-2 control-label" for="resource.ownerManagedAccess">User-Managed Access Enabled</label>
+ <label class="col-md-2 control-label" for="resource.ownerManagedAccess">{{:: 'authz-resource-user-managed-access-enabled' | translate}}</label>
<div class="col-md-6">
<input ng-model="resource.ownerManagedAccess" id="resource.ownerManagedAccess" onoffswitch />
</div>
- <kc-tooltip>{{:: 'authz-permission-resource-apply-to-resource-type.tooltip' | translate}}</kc-tooltip>
+ <kc-tooltip>{{:: 'authz-resource-user-managed-access-enabled.tooltip' | translate}}</kc-tooltip>
+ </div>
+ <div class="form-group">
+ <label class="col-md-2 control-label">{{:: 'authz-resource-attributes' | translate}}</label>
+ <div class="col-md-6">
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>{{:: 'key' | translate}}</th>
+ <th>{{:: 'value' | translate}}</th>
+ <th>{{:: 'actions' | translate}}</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="(key, value) in resource.attributes | toOrderedMapSortedByKey">
+ <td>{{key}}</td>
+ <td><input ng-model="resource.attributes[key]" class="form-control" type="text" name="{{key}}" id="attribute-{{key}}" /></td>
+ <td class="kc-action-cell" id="removeAttribute" data-ng-click="removeAttribute(key)">{{:: 'delete' | translate}}</td>
+ </tr>
+ <tr>
+ <td><input ng-model="newAttribute.key" class="form-control" type="text" id="newAttributeKey" /></td>
+ <td><input ng-model="newAttribute.value" class="form-control" type="text" id="newAttributeValue" /></td>
+ <td class="kc-action-cell" id="addAttribute" data-ng-click="addAttribute()" data-ng-disabled="!newAttribute.key.length || !newAttribute.value.length">{{:: 'add' | translate}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <kc-tooltip>{{:: 'authz-resource-attributes.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>