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 b0b69484c5..65ec87c321 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 @@ -115,6 +115,9 @@ public class ResourceRepresentation { } public Set getScopes() { + if (this.scopes == null) { + this.scopes = Collections.emptySet(); + } return Collections.unmodifiableSet(this.scopes); } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java index e237642ed6..91c78a2f44 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/ProtectedResource.java @@ -48,6 +48,16 @@ public class ProtectedResource { } } + public void update(ResourceRepresentation resource) { + try { + this.http.put("/authz/protection/resource_set/" + resource.getId()) + .authorizationBearer(this.pat) + .json(JsonSerialization.writeValueAsBytes(resource)).execute(); + } catch (Exception e) { + throw new RuntimeException("Could not create resource.", e); + } + } + public RegistrationResponse findById(String id) { try { return this.http.get("/authz/protection/resource_set/" + id) diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java index 0982abf8f2..8bd9c0783f 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/util/Http.java @@ -51,6 +51,10 @@ public class Http { return method(RequestBuilder.post().setUri(this.serverConfiguration.getIssuer() + path)); } + public HttpMethod put(String path) { + return method(RequestBuilder.put().setUri(this.serverConfiguration.getIssuer() + path)); + } + public HttpMethod delete(String path) { return method(RequestBuilder.delete().setUri(this.serverConfiguration.getIssuer() + path)); } diff --git a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java index 493637aeaa..ea37d60e9a 100644 --- a/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java +++ b/examples/authz/hello-world/src/main/java/org/keycloak/authz/helloworld/AuthorizationClientExample.java @@ -39,6 +39,7 @@ public class AuthorizationClientExample { obtainEntitlementsForResource(); obtainAllEntitlements(); createResource(); + updateResource(); introspectRequestingPartyToken(); } @@ -109,6 +110,34 @@ public class AuthorizationClientExample { System.out.println(resource); } + private static void updateResource() { + // create a new instance based on the configuration defined in keycloak-authz.json + AuthzClient authzClient = AuthzClient.create(); + + // create a new resource representation with the information we want + ResourceRepresentation resource = new ResourceRepresentation(); + + resource.setName("New Resource"); + + ProtectedResource resourceClient = authzClient.protection().resource(); + Set existingResource = resourceClient.findByFilter("name=" + resource.getName()); + + if (existingResource.isEmpty()) { + createResource(); + } + + resource.setId(existingResource.iterator().next()); + resource.setUri("Changed URI"); + + // update the resource on the server + resourceClient.update(resource); + + // query the resource using its newly generated id + ResourceRepresentation existing = resourceClient.findById(resource.getId()).getResourceDescription(); + + System.out.println(existing); + } + private static void obtainEntitlementsForResource() { // create a new instance based on the configuration define at keycloak-authz.json AuthzClient authzClient = AuthzClient.create(); 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 3573d3ddf9..1695ba53ca 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 @@ -17,9 +17,27 @@ */ package org.keycloak.authorization.protection.resource; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.admin.ResourceSetService; import org.keycloak.authorization.identity.Identity; +import org.keycloak.authorization.model.Resource; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.protection.resource.representation.UmaResourceRepresentation; import org.keycloak.authorization.protection.resource.representation.UmaScopeRepresentation; @@ -30,21 +48,6 @@ import org.keycloak.representations.idm.authorization.ResourceRepresentation; import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.services.ErrorResponseException; -import javax.ws.rs.Consumes; -import javax.ws.rs.DELETE; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - /** * @author Pedro Igor */ @@ -77,6 +80,21 @@ public class ResourceService { return response; } + @Path("{id}") + @PUT + @Consumes("application/json") + @Produces("application/json") + public Response update(@PathParam("id") String id, UmaResourceRepresentation representation) { + ResourceRepresentation resource = toResourceRepresentation(representation); + Response response = this.resourceManager.update(id, resource); + + if (response.getEntity() instanceof ResourceRepresentation) { + return Response.noContent().build(); + } + + return response; + } + @Path("/{id}") @DELETE public Response delete(@PathParam("id") String id) { @@ -132,7 +150,11 @@ public class ResourceService { if ("name".equals(filterType)) { - resources.add(ModelToRepresentation.toRepresentation(storeFactory.getResourceStore().findByName(filterValue, this.resourceServer.getId()), resourceServer, authorization)); + Resource resource = storeFactory.getResourceStore().findByName(filterValue, this.resourceServer.getId()); + + if (resource != null) { + resources.add(ModelToRepresentation.toRepresentation(resource, resourceServer, authorization)); + } } else if ("type".equals(filterType)) { resources.addAll(storeFactory.getResourceStore().findByResourceServer(this.resourceServer.getId()).stream().filter(description -> filterValue == null || filterValue.equals(description.getType())).collect(Collectors.toSet()).stream() .map(resource -> ModelToRepresentation.toRepresentation(resource, this.resourceServer, authorization)) 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 0e9bafae51..2997498ac6 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 @@ -17,6 +17,7 @@ */ package org.keycloak.authorization.protection.resource.representation; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.net.URI; @@ -38,6 +39,8 @@ public class UmaResourceRepresentation { private String name; private String uri; private String type; + + @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set scopes; @JsonProperty("icon_uri") diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java index c46b338957..642e116fd9 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AbstractPhotozAdminTest.java @@ -89,7 +89,11 @@ public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest StoreFactory storeFactory = authorizationProvider.getStoreFactory(); ResourceServerStore resourceServerStore = storeFactory.getResourceServerStore(); - return resourceServerStore.create(getClientByClientId("photoz-restful-api").getId()); + ResourceServer resourceServer = resourceServerStore.create(getClientByClientId("photoz-restful-api").getId()); + + resourceServer.setAllowRemoteResourceManagement(true); + + return resourceServer; }); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AuthorizationClientTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AuthorizationClientTest.java new file mode 100644 index 0000000000..aa3dc57d86 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/authorization/AuthorizationClientTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2016 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.testsuite.authorization; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +import java.util.Collections; +import java.util.Set; + +import org.junit.Test; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.representation.RegistrationResponse; +import org.keycloak.authorization.client.representation.ResourceRepresentation; +import org.keycloak.authorization.client.representation.ScopeRepresentation; +import org.keycloak.authorization.client.resource.ProtectedResource; +import org.keycloak.authorization.client.util.HttpResponseException; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class AuthorizationClientTest extends AbstractPhotozAdminTest { + + @Test + public void testCreate() throws Exception { + AuthzClient authzClient = getAuthzClient(); + // create a new resource representation with the information we want + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + newResource.setType("urn:hello-world-authz:resources:example"); + + newResource.addScope(new ScopeRepresentation("urn:hello-world-authz:scopes:view")); + + ProtectedResource resourceClient = authzClient.protection().resource(); + Set existingResource = resourceClient.findByFilter("name=" + newResource.getName()); + + if (!existingResource.isEmpty()) { + resourceClient.delete(existingResource.iterator().next()); + } + + // create the resource on the server + RegistrationResponse response = resourceClient.create(newResource); + String resourceId = response.getId(); + + // query the resource using its newly generated id + ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertNotNull(resource.getId()); + assertEquals("New Resource", resource.getName()); + } + + @Test + public void testUpdate() throws Exception { + AuthzClient authzClient = getAuthzClient(); + // create a new resource representation with the information we want + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + + ProtectedResource protectedResource = authzClient.protection().resource(); + ProtectedResource resourceClient = protectedResource; + Set existingResource = resourceClient.findByFilter("name=" + newResource.getName()); + + if (!existingResource.isEmpty()) { + resourceClient.delete(existingResource.iterator().next()); + } + + // create the resource on the server + RegistrationResponse response = resourceClient.create(newResource); + String resourceId = response.getId(); + + // query the resource using its newly generated id + ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertNotNull(resource.getId()); + assertEquals("New Resource", resource.getName()); + + resource.setType("Changed Type"); + resource.setUri("Changed Uri"); + resource.addScope(new ScopeRepresentation("new-scope")); + + protectedResource.update(resource); + + resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertEquals("Changed Type", resource.getType()); + assertEquals("Changed Uri", resource.getUri()); + + Set scopes = resource.getScopes(); + + assertEquals(1, scopes.size()); + assertEquals("new-scope", scopes.iterator().next().getName()); + + resource.setScopes(Collections.emptySet()); + + protectedResource.update(resource); + + resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertEquals(0, resource.getScopes().size()); + + resource.setScopes(scopes); + + protectedResource.update(resource); + + resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertEquals(1, resource.getScopes().size()); + assertEquals(scopes.iterator().next().getId(), resource.getScopes().iterator().next().getId()); + } + + @Test + public void testDelete() throws Exception { + AuthzClient authzClient = getAuthzClient(); + // create a new resource representation with the information we want + ResourceRepresentation newResource = new ResourceRepresentation(); + + newResource.setName("New Resource"); + + ProtectedResource resourceClient = authzClient.protection().resource(); + Set existingResource = resourceClient.findByFilter("name=" + newResource.getName()); + + if (!existingResource.isEmpty()) { + resourceClient.delete(existingResource.iterator().next()); + } + + // create the resource on the server + RegistrationResponse response = resourceClient.create(newResource); + String resourceId = response.getId(); + + // query the resource using its newly generated id + ResourceRepresentation resource = resourceClient.findById(resourceId).getResourceDescription(); + + assertNotNull(resource); + + authzClient.protection().resource().delete(resource.getId()); + + try { + resourceClient.findById(resourceId); + fail(); + } catch (RuntimeException e) { + HttpResponseException cause = (HttpResponseException) e.getCause(); + assertEquals(404, cause.getStatusCode()); + } + } + + private AuthzClient getAuthzClient() throws java.io.IOException { + Configuration configuration = JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/keycloak.json"), Configuration.class); + return AuthzClient.create(configuration); + } +} diff --git a/testsuite/integration/src/test/resources/authorization-test/keycloak.json b/testsuite/integration/src/test/resources/authorization-test/keycloak.json new file mode 100644 index 0000000000..1dffb5a5b8 --- /dev/null +++ b/testsuite/integration/src/test/resources/authorization-test/keycloak.json @@ -0,0 +1,8 @@ +{ + "realm": "photoz", + "auth-server-url" : "http://localhost:8081/auth", + "resource" : "photoz-restful-api", + "credentials": { + "secret": "secret" + } +} \ No newline at end of file diff --git a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json index 4e300e4e20..8abc78c5d4 100644 --- a/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json +++ b/testsuite/integration/src/test/resources/authorization-test/test-photoz-realm.json @@ -91,7 +91,10 @@ "serviceAccountClientId": "photoz-restful-api", "realmRoles": [ "uma_protection" - ] + ], + "clientRoles": { + "photoz-restful-api" : ["uma_protection"] + } } ], "roles": {