Merge pull request #5079 from pedroigor/KEYCLOAK-6529

[KEYCLOAK-6529] - Resource Attributes
This commit is contained in:
Pedro Igor 2018-03-27 09:30:38 -03:00 committed by GitHub
commit ffeb0420bf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 549 additions and 8 deletions

View file

@ -20,6 +20,8 @@ package org.keycloak.authorization.client.representation;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
@ -49,6 +51,7 @@ public class ResourceRepresentation {
private String iconUri; private String iconUri;
private String owner; private String owner;
private Boolean ownerManagedAccess; private Boolean ownerManagedAccess;
private Map<String, List<String>> attributes;
/** /**
* Creates a new instance. * Creates a new instance.
@ -204,4 +207,12 @@ public class ResourceRepresentation {
", scopes=" + scopes + ", scopes=" + scopes +
'}'; '}';
} }
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
public Map<String, List<String>> getAttributes() {
return attributes;
}
} }

View file

@ -20,11 +20,14 @@ import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty; 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. * <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 List<PolicyRepresentation> policies;
private String displayName; private String displayName;
@JsonDeserialize(using = StringListMapDeserializer.class)
private Map<String, List<String>> attributes;
/** /**
* Creates a new instance. * 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) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;

View file

@ -27,6 +27,7 @@ import org.keycloak.models.cache.infinispan.authorization.entities.CachedResourc
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -35,6 +36,7 @@ import java.util.stream.Collectors;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ResourceAdapter implements Resource, CachedModel<Resource> { public class ResourceAdapter implements Resource, CachedModel<Resource> {
protected CachedResource cached; protected CachedResource cached;
protected StoreFactoryCacheSession cacheSession; protected StoreFactoryCacheSession cacheSession;
protected Resource updated; protected Resource updated;
@ -210,6 +212,50 @@ public class ResourceAdapter implements Resource, CachedModel<Resource> {
updated.updateScopes(scopes); updated.updateScopes(scopes);
} }
@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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -20,8 +20,11 @@ package org.keycloak.models.cache.infinispan.authorization.entities;
import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned; import org.keycloak.models.cache.infinispan.entities.AbstractRevisioned;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -39,6 +42,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
private String uri; private String uri;
private Set<String> scopesIds; private Set<String> scopesIds;
private boolean ownerManagedAccess; private boolean ownerManagedAccess;
private MultivaluedHashMap<String, String> attributes = new MultivaluedHashMap<>();
public CachedResource(Long revision, Resource resource) { public CachedResource(Long revision, Resource resource) {
super(revision, resource.getId()); super(revision, resource.getId());
@ -51,6 +55,7 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
this.resourceServerId = resource.getResourceServer().getId(); this.resourceServerId = resource.getResourceServer().getId();
this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet()); this.scopesIds = resource.getScopes().stream().map(Scope::getId).collect(Collectors.toSet());
ownerManagedAccess = resource.isOwnerManagedAccess(); ownerManagedAccess = resource.isOwnerManagedAccess();
this.attributes.putAll(resource.getAttributes());
} }
@ -89,4 +94,8 @@ public class CachedResource extends AbstractRevisioned implements InResourceServ
public Set<String> getScopesIds() { public Set<String> getScopesIds() {
return this.scopesIds; return this.scopesIds;
} }
public Map<String, List<String>> getAttributes() {
return attributes;
}
} }

View file

@ -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();
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.authorization.jpa.entities;
import javax.persistence.Access; import javax.persistence.Access;
import javax.persistence.AccessType; import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.FetchType; import javax.persistence.FetchType;
@ -30,11 +31,18 @@ import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries; import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery; import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.UniqueConstraint; import javax.persistence.UniqueConstraint;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
/** /**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a> * @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")) @JoinTable(name = "RESOURCE_POLICY", joinColumns = @JoinColumn(name = "RESOURCE_ID"), inverseJoinColumns = @JoinColumn(name = "POLICY_ID"))
private List<PolicyEntity> policies = new LinkedList<>(); 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() { public String getId() {
return id; return id;
} }
@ -179,6 +191,14 @@ public class ResourceEntity {
this.policies = policies; this.policies = policies;
} }
public Collection<ResourceAttributeEntity> getAttributes() {
return attributes;
}
public void setAttributes(Collection<ResourceAttributeEntity> attributes) {
this.attributes = attributes;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View file

@ -16,20 +16,27 @@
*/ */
package org.keycloak.authorization.jpa.store; 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.ResourceEntity;
import org.keycloak.authorization.jpa.entities.ScopeEntity; import org.keycloak.authorization.jpa.entities.ScopeEntity;
import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope; import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.store.StoreFactory; import org.keycloak.authorization.store.StoreFactory;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.jpa.JpaModel; import org.keycloak.models.jpa.JpaModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.Query;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -37,6 +44,7 @@ import java.util.Set;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> { public class ResourceAdapter implements Resource, JpaModel<ResourceEntity> {
private ResourceEntity entity; private ResourceEntity entity;
private EntityManager em; private EntityManager em;
private StoreFactory storeFactory; 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) { public static ResourceEntity toEntity(EntityManager em, Resource resource) {
if (resource instanceof ResourceAdapter) { if (resource instanceof ResourceAdapter) {

View file

@ -74,5 +74,21 @@
<constraints nullable="false"/> <constraints nullable="false"/>
</column> </column>
</addColumn> </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> </changeSet>
</databaseChangeLog> </databaseChangeLog>

View file

@ -67,6 +67,7 @@
<class>org.keycloak.authorization.jpa.entities.ScopeEntity</class> <class>org.keycloak.authorization.jpa.entities.ScopeEntity</class>
<class>org.keycloak.authorization.jpa.entities.PolicyEntity</class> <class>org.keycloak.authorization.jpa.entities.PolicyEntity</class>
<class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class> <class>org.keycloak.authorization.jpa.entities.PermissionTicketEntity</class>
<class>org.keycloak.authorization.jpa.entities.ResourceAttributeEntity</class>
<!-- User Federation Storage --> <!-- User Federation Storage -->
<class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class> <class>org.keycloak.storage.jpa.entity.BrokerLinkEntity</class>

View file

@ -19,6 +19,7 @@
package org.keycloak.authorization.model; package org.keycloak.authorization.model;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -126,8 +127,56 @@ public interface Resource {
*/ */
String getOwner(); 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(); 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); 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); 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);
} }

View file

@ -841,6 +841,8 @@ public class ModelToRepresentation {
} }
return scope; return scope;
}).collect(Collectors.toSet())); }).collect(Collectors.toSet()));
resource.setAttributes(new HashMap<>(model.getAttributes()));
} }
return resource; return resource;

View file

@ -26,6 +26,7 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -2351,6 +2352,24 @@ public class RepresentationToModel {
existing.updateScopes(resource.getScopes().stream() existing.updateScopes(resource.getScopes().stream()
.map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization)) .map((ScopeRepresentation scope) -> toModel(scope, resourceServer, authorization))
.collect(Collectors.toSet())); .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; 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())); 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()); resource.setId(model.getId());
return model; return model;

View file

@ -294,6 +294,22 @@ public class ResourceSetService {
return Response.ok(representation).build(); 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") @Path("/search")
@GET @GET
@NoCache @NoCache

View file

@ -150,6 +150,8 @@ public class ResourceService {
return scopeRepresentation; return scopeRepresentation;
}).collect(Collectors.toSet())); }).collect(Collectors.toSet()));
resource.setAttributes(umaResource.getAttributes());
return resource; return resource;
} }
@ -178,6 +180,8 @@ public class ResourceService {
return umaScopeRep; return umaScopeRep;
}).collect(Collectors.toSet())); }).collect(Collectors.toSet()));
resource.setAttributes(model.getAttributes());
return resource; return resource;
} }

View file

@ -22,6 +22,8 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
/** /**
@ -49,6 +51,8 @@ public class UmaResourceRepresentation {
private String owner; private String owner;
private Boolean ownerManagedAccess; private Boolean ownerManagedAccess;
private Map<String, List<String>> attributes;
/** /**
* Creates a new instance. * Creates a new instance.
@ -161,4 +165,12 @@ public class UmaResourceRepresentation {
public Boolean getOwnerManagedAccess() { public Boolean getOwnerManagedAccess() {
return ownerManagedAccess; return ownerManagedAccess;
} }
public Map<String, List<String>> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes = attributes;
}
} }

View file

@ -28,8 +28,12 @@ import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import javax.ws.rs.NotFoundException; import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
@ -53,6 +57,17 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
assertEquals("/test/*", newResource.getUri()); assertEquals("/test/*", newResource.getUri());
assertEquals("test-resource", newResource.getType()); assertEquals("test-resource", newResource.getType());
assertEquals("icon-test-resource", newResource.getIconUri()); 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 @Test
@ -105,11 +120,28 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
resource.setIconUri("changed"); resource.setIconUri("changed");
resource.setUri("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); resource = doUpdateResource(resource);
assertEquals("changed", resource.getIconUri()); assertEquals("changed", resource.getIconUri());
assertEquals("changed", resource.getType()); assertEquals("changed", resource.getType());
assertEquals("changed", resource.getUri()); 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) @Test(expected = NotFoundException.class)
@ -205,6 +237,13 @@ public class ResourceManagementTest extends AbstractAuthorizationTest {
newResource.setIconUri(iconUri); newResource.setIconUri(iconUri);
newResource.setOwner(owner != null ? new ResourceOwnerRepresentation(owner) : null); 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); return doCreateResource(newResource);
} }

View file

@ -83,6 +83,8 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
return scope; return scope;
}).collect(Collectors.toSet())); }).collect(Collectors.toSet()));
resourceRepresentation.setAttributes(created.getAttributes());
return resourceRepresentation; return resourceRepresentation;
} }
@ -108,6 +110,8 @@ public class ResourceManagementWithAuthzClientTest extends ResourceManagementTes
return scope; return scope;
}).collect(Collectors.toSet())); }).collect(Collectors.toSet()));
resource.setAttributes(newResource.getAttributes());
return resource; return resource;
} }

View file

@ -35,6 +35,7 @@ import org.keycloak.authorization.attribute.Attributes;
import org.keycloak.authorization.common.DefaultEvaluationContext; import org.keycloak.authorization.common.DefaultEvaluationContext;
import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.identity.Identity;
import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Policy;
import org.keycloak.authorization.model.Resource;
import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.permission.ResourcePermission; import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation; import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
@ -556,9 +557,49 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
Assert.assertEquals(Effect.PERMIT, evaluation.getEffect()); 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) { 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 @Override
public String getId() { public String getId() {
return null; return null;
@ -568,11 +609,8 @@ public class PolicyEvaluationTest extends AbstractAuthzTest {
public Attributes getAttributes() { public Attributes getAttributes() {
return null; return null;
} }
}, session), policy, policy, new Decision() { }, session), policy, policy, evaluation -> {
@Override
public void onDecision(Evaluation evaluation) {
}
}, authorization); }, authorization);
} }
} }

View file

@ -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-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=An URI that can also be used to uniquely identify this resource.
authz-resource-scopes.tooltip=The scopes associated with 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 Scope List
authz-add-scope=Add Scope authz-add-scope=Add Scope

View file

@ -294,6 +294,7 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
var resource = {}; var resource = {};
resource.scopes = []; resource.scopes = [];
resource.attributes = {};
$scope.resource = angular.copy(resource); $scope.resource = angular.copy(resource);
@ -328,6 +329,10 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
data.scopes = []; data.scopes = [];
} }
if (!data.attributes) {
data.attributes = {};
}
$scope.resource = angular.copy(data); $scope.resource = angular.copy(data);
$scope.changed = false; $scope.changed = false;
@ -343,6 +348,15 @@ module.controller('ResourceServerResourceDetailCtrl', function($scope, $http, $r
for (i = 0; i < $scope.resource.scopes.length; i++) { for (i = 0; i < $scope.resource.scopes.length; i++) {
delete $scope.resource.scopes[i].text; 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 () { $instance.checkNameAvailability(function () {
ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() { ResourceServerResource.update({realm : realm.realm, client : $scope.client.id, rsrid : $scope.resource._id}, $scope.resource, function() {
$route.reload(); $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 = { var Scopes = {

View file

@ -67,11 +67,38 @@
<kc-tooltip>{{:: 'authz-icon-uri.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'authz-icon-uri.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group"> <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"> <div class="col-md-6">
<input ng-model="resource.ownerManagedAccess" id="resource.ownerManagedAccess" onoffswitch /> <input ng-model="resource.ownerManagedAccess" id="resource.ownerManagedAccess" onoffswitch />
</div> </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> </div>
</fieldset> </fieldset>