diff --git a/core/src/main/java/org/keycloak/representations/account/ConsentRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ConsentRepresentation.java new file mode 100644 index 0000000000..318e72c275 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/account/ConsentRepresentation.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 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.representations.account; + +import java.util.List; + +public class ConsentRepresentation { + + private List scopes; + + private Long createdDate; + + private Long lastUpdatedDate; + + public ConsentRepresentation() { + } + + public ConsentRepresentation(List scopes, Long createdDate, Long lastUpdatedDate) { + this.scopes = scopes; + this.createdDate = createdDate; + this.lastUpdatedDate = lastUpdatedDate; + } + + public List getScopes() { + return scopes; + } + + public void setScopes(List scopes) { + this.scopes = scopes; + } + + public Long getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Long createdDate) { + this.createdDate = createdDate; + } + + public Long getLastUpdatedDate() { + return lastUpdatedDate; + } + + public void setLastUpdatedDate(Long lastUpdatedDate) { + this.lastUpdatedDate = lastUpdatedDate; + } +} diff --git a/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java new file mode 100644 index 0000000000..a4b6400a16 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java @@ -0,0 +1,60 @@ +/* + * Copyright 2019 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.representations.account; + +public class ConsentScopeRepresentation { + + private String id; + + private String name; + + private String displayTest; + + public ConsentScopeRepresentation() { + } + + public ConsentScopeRepresentation(String id, String name, String displayTest) { + this.id = id; + this.name = name; + this.displayTest = displayTest; + } + + 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 getDisplayTest() { + return displayTest; + } + + public void setDisplayTest(String displayTest) { + this.displayTest = displayTest; + } +} diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventType.java b/server-spi-private/src/main/java/org/keycloak/events/EventType.java index 29b38bcbae..e063677ab9 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventType.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventType.java @@ -67,6 +67,10 @@ public enum EventType { REMOVE_TOTP(true), REMOVE_TOTP_ERROR(true), + GRANT_CONSENT(true), + GRANT_CONSENT_ERROR(true), + UPDATE_CONSENT(true), + UPDATE_CONSENT_ERROR(true), REVOKE_GRANT(true), REVOKE_GRANT_ERROR(true), diff --git a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java index 27c5893e2c..098507d278 100644 --- a/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java +++ b/server-spi-private/src/main/java/org/keycloak/models/AccountRoles.java @@ -26,6 +26,9 @@ public interface AccountRoles { String MANAGE_ACCOUNT = "manage-account"; String INITIATE_ACTION = "initiate-action"; String MANAGE_ACCOUNT_LINKS = "manage-account-links"; + String VIEW_APPLICATIONS = "view-applications"; + String VIEW_CONSENT = "view-consent"; + String MANAGE_CONSENT = "manage-consent"; String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT}; diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index 727a9ae4af..92994a89e0 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -19,17 +19,22 @@ package org.keycloak.services.resources.account; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.common.ClientConnection; +import org.keycloak.common.Profile; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; import org.keycloak.models.AccountRoles; import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientScopeModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.representations.account.ClientRepresentation; +import org.keycloak.representations.account.ConsentRepresentation; +import org.keycloak.representations.account.ConsentScopeRepresentation; import org.keycloak.representations.account.SessionRepresentation; import org.keycloak.representations.account.UserRepresentation; import org.keycloak.services.ErrorResponse; @@ -39,16 +44,30 @@ import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.account.resources.ResourcesService; import org.keycloak.storage.ReadOnlyException; +import org.keycloak.theme.Theme; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.OPTIONS; +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.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.io.IOException; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; -import org.keycloak.common.Profile; +import java.util.Properties; +import java.util.stream.Collectors; /** * @author Stian Thorgersen @@ -70,6 +89,7 @@ public class AccountRestService { private final RealmModel realm; private final UserModel user; + private final Locale locale; public AccountRestService(KeycloakSession session, Auth auth, ClientModel client, EventBuilder event) { this.session = session; @@ -78,6 +98,7 @@ public class AccountRestService { this.user = auth.getUser(); this.client = client; this.event = event; + this.locale = session.getContext().resolveLocale(user); } public void init() { @@ -296,8 +317,231 @@ public class AccountRestService { return new ResourcesService(session, user, auth, request); } - // TODO Federated identities - // TODO Applications + // TODO Federated identities + + /** + * Returns the list of available applications in the specified + * realm. + * + * @return list of applications in that realm + */ + @Path("/applications") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getApplications() { + checkAccountApiEnabled(); + auth.require(AccountRoles.VIEW_APPLICATIONS); + + List clients = realm.getClients(); + + List clientRepresentations = clients.stream() + .map(this::modelToRepresentation) + .collect(Collectors.toList()); + + return Cors.add(request, Response.ok(clientRepresentations)).build(); + } + + /** + * Returns the applications with the given id in the specified realm. + * + * @param clientId client id to search for + * @return application with the provided id + */ + @Path("/applications/{clientId}") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getApplication(final @PathParam("clientId") String clientId) { + checkAccountApiEnabled(); + auth.require(AccountRoles.VIEW_APPLICATIONS); + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity("No client with clientId: " + clientId + " found.")).build(); + } + + return Cors.add(request, Response.ok(modelToRepresentation(client))).build(); + } + + private ClientRepresentation modelToRepresentation(ClientModel model) { + ClientRepresentation representation = new ClientRepresentation(); + representation.setClientId(model.getClientId()); + representation.setClientName(getTranslationOrDefault(model.getName())); + return representation; + } + + private ConsentRepresentation modelToRepresentation(UserConsentModel model) { + List scopes = model.getGrantedClientScopes().stream() + .map(m -> new ConsentScopeRepresentation(m.getId(), m.getName(), getTranslationOrDefault(m.getConsentScreenText()))) + .collect(Collectors.toList()); + return new ConsentRepresentation(scopes, model.getCreatedDate(), model.getLastUpdatedDate()); + } + + private String getTranslationOrDefault(String key) { + if (key == null) { + return null; + } + String defaultValue = key; + if (key.startsWith("${")) { + key = key.substring(2, key.length() - 1); + } + try { + Properties messages = session.theme().getTheme(Theme.Type.ACCOUNT).getMessages(locale); + return messages.getProperty(key, defaultValue); + } catch (IOException e) { + return key; + } + } + + /** + * Returns the consent for the client with the given client id. + * + * @param clientId client id to return the consent for + * @return consent of the client + */ + @Path("/applications/{clientId}/consent") + @GET + @Produces(MediaType.APPLICATION_JSON) + public Response getConsent(final @PathParam("clientId") String clientId) { + checkAccountApiEnabled(); + auth.requireOneOf(AccountRoles.VIEW_CONSENT, AccountRoles.MANAGE_CONSENT); + + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity("No client with clientId: " + clientId + " found.")).build(); + } + + UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId()); + if (consent == null) { + return Cors.add(request, Response.noContent()).build(); + } + + return Cors.add(request, Response.ok(modelToRepresentation(consent))).build(); + } + + /** + * Deletes the consent for the client with the given client id. + * + * @param clientId client id to delete a consent for + * @return returns 202 if deleted + */ + @Path("/applications/{clientId}/consent") + @DELETE + public Response revokeConsent(final @PathParam("clientId") String clientId) { + checkAccountApiEnabled(); + auth.require(AccountRoles.MANAGE_CONSENT); + + event.event(EventType.REVOKE_GRANT); + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + event.event(EventType.REVOKE_GRANT_ERROR); + String msg = String.format("No client with clientId: %s found.", clientId); + event.error(msg); + return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build(); + } + + session.users().revokeConsentForClient(realm, user.getId(), client.getId()); + event.success(); + + return Cors.add(request, Response.accepted()).build(); + } + + /** + * Creates or updates the consent of the given, requested consent for + * the client with the given client id. Returns the appropriate REST response. + * + * @param clientId client id to set a consent for + * @param consent requested consent for the client + * @return the created or updated consent + */ + @Path("/applications/{clientId}/consent") + @POST + @Produces(MediaType.APPLICATION_JSON) + public Response grantConsent(final @PathParam("clientId") String clientId, + final ConsentRepresentation consent) { + return upsert(clientId, consent); + } + + /** + * Creates or updates the consent of the given, requested consent for + * the client with the given client id. Returns the appropriate REST response. + * + * @param clientId client id to set a consent for + * @param consent requested consent for the client + * @return the created or updated consent + */ + @Path("/applications/{clientId}/consent") + @PUT + @Produces(MediaType.APPLICATION_JSON) + public Response updateConsent(final @PathParam("clientId") String clientId, + final ConsentRepresentation consent) { + return upsert(clientId, consent); + } + + /** + * Creates or updates the consent of the given, requested consent for + * the client with the given client id. Returns the appropriate REST response. + * + * @param clientId client id to set a consent for + * @param consent requested consent for the client + * @return response to return to the caller + */ + private Response upsert(String clientId, ConsentRepresentation consent) { + checkAccountApiEnabled(); + auth.require(AccountRoles.MANAGE_CONSENT); + + event.event(EventType.GRANT_CONSENT); + ClientModel client = realm.getClientByClientId(clientId); + if (client == null) { + event.event(EventType.GRANT_CONSENT_ERROR); + String msg = String.format("No client with clientId: %s found.", clientId); + event.error(msg); + return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build(); + } + + try { + UserConsentModel grantedConsent = createConsent(client, consent); + if (session.users().getConsentByClient(realm, user.getId(), client.getId()) == null) { + session.users().addConsent(realm, user.getId(), grantedConsent); + } else { + session.users().updateConsent(realm, user.getId(), grantedConsent); + } + event.success(); + grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId()); + return Cors.add(request, Response.ok(modelToRepresentation(grantedConsent))).build(); + } catch (IllegalArgumentException e) { + return Cors.add(request, Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage())).build(); + } + } + + /** + * Create a new consent model object from the requested consent object + * for the given client model. + * + * @param client client to create a consent for + * @param requested list of client scopes that the new consent should contain + * @return newly created consent model + * @throws IllegalArgumentException throws an exception if the scope id is not available + */ + private UserConsentModel createConsent(ClientModel client, ConsentRepresentation requested) throws IllegalArgumentException { + UserConsentModel consent = new UserConsentModel(client); + Map availableGrants = realm.getClientScopes().stream().collect(Collectors.toMap(ClientScopeModel::getId, s -> s)); + + if (client.isConsentRequired()) { + availableGrants.put(client.getId(), client); + } + + for (ConsentScopeRepresentation scopeRepresentation : requested.getScopes()) { + ClientScopeModel scopeModel = availableGrants.get(scopeRepresentation.getId()); + if (scopeModel == null) { + String msg = String.format("Scope id %s does not exist for client %s.", scopeRepresentation, consent.getClient().getName()); + event.error(msg); + throw new IllegalArgumentException(msg); + } else { + consent.addGrantedClientScope(scopeModel); + } + } + return consent; + } + // TODO Logs private static void checkAccountApiEnabled() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AbstractRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AbstractRestServiceTest.java index 887c7b5c6e..ccb56410a3 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AbstractRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AbstractRestServiceTest.java @@ -87,6 +87,9 @@ public abstract class AbstractRestServiceTest extends AbstractTestRealmKeycloakT public void configureTestRealm(RealmRepresentation testRealm) { testRealm.getUsers().add(UserBuilder.create().username("no-account-access").password("password").build()); testRealm.getUsers().add(UserBuilder.create().username("view-account-access").role("account", "view-profile").password("password").build()); + testRealm.getUsers().add(UserBuilder.create().username("view-applications-access").role("account", "view-applications").password("password").build()); + testRealm.getUsers().add(UserBuilder.create().username("view-consent-access").role("account", "view-consent").password("password").build()); + testRealm.getUsers().add(UserBuilder.create().username("manage-consent-access").role("account", "manage-consent").password("password").build()); } protected String getAccountUrl(String resource) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index 1c82de935c..5a84edd154 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -19,8 +19,12 @@ package org.keycloak.testsuite.account; import com.fasterxml.jackson.core.type.TypeReference; import org.junit.Test; import org.keycloak.broker.provider.util.SimpleHttp; +import org.keycloak.representations.account.ClientRepresentation; +import org.keycloak.representations.account.ConsentRepresentation; +import org.keycloak.representations.account.ConsentScopeRepresentation; import org.keycloak.representations.account.SessionRepresentation; import org.keycloak.representations.account.UserRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.messages.Messages; @@ -333,4 +337,459 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { sessions = SimpleHttp.doGet(getAccountUrl("sessions"), httpClient).auth(tokenUtil.getToken()).asJson(new TypeReference>() {}); assertEquals(1, sessions.size()); } + + @Test + public void listApplications() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-applications-access", "password"); + List applications = SimpleHttp + .doGet(getAccountUrl("applications"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asJson(new TypeReference>() { + }); + assertFalse(applications.isEmpty()); + } + + @Test + public void listApplicationsWithoutPermission() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-account-access", "password"); + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(403, response.getStatus()); + } + + @Test + public void getWebConsoleApplication() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-applications-access", "password"); + String appId = "security-admin-console"; + ClientRepresentation webConsole = SimpleHttp + .doGet(getAccountUrl("applications/" + appId), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asJson(ClientRepresentation.class); + assertEquals(appId, webConsole.getClientId()); + } + + @Test + public void getWebConsoleApplicationWithoutPermission() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-account-access", "password"); + String appId = "security-admin-console"; + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications/" + appId), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(403, response.getStatus()); + } + + @Test + public void getNotExistingApplication() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-applications-access", "password"); + String appId = "not-existing"; + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications/" + appId), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(404, response.getStatus()); + } + + @Test + public void createConsentForClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation.getCreatedDate() > 0); + assertTrue(consentRepresentation.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation.getScopes().get(0).getId()); + } + + @Test + public void updateConsentForClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation.getCreatedDate() > 0); + assertTrue(consentRepresentation.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation.getScopes().get(0).getId()); + + clientScopeRepresentation = testRealm().clientScopes().findAll().get(1); + consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation2 = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation2.getCreatedDate() > 0); + assertEquals(consentRepresentation.getCreatedDate(), consentRepresentation2.getCreatedDate()); + assertTrue(consentRepresentation2.getLastUpdatedDate() > 0); + assertTrue(consentRepresentation2.getLastUpdatedDate() > consentRepresentation.getLastUpdatedDate()); + assertEquals(1, consentRepresentation2.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation2.getScopes().get(0).getId()); + } + + @Test + public void createConsentForNotExistingClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "not-existing"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + SimpleHttp.Response response = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asResponse(); + + assertEquals(404, response.getStatus()); + } + + @Test + public void createConsentForClientWithoutPermission() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + SimpleHttp.Response response = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asResponse(); + + assertEquals(403, response.getStatus()); + } + + @Test + public void createConsentForClientWithPut() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation = SimpleHttp + .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation.getCreatedDate() > 0); + assertTrue(consentRepresentation.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation.getScopes().get(0).getId()); + } + + @Test + public void updateConsentForClientWithPut() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation = SimpleHttp + .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation.getCreatedDate() > 0); + assertTrue(consentRepresentation.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation.getScopes().get(0).getId()); + + clientScopeRepresentation = testRealm().clientScopes().findAll().get(1); + consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation2 = SimpleHttp + .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation2.getCreatedDate() > 0); + assertEquals(consentRepresentation.getCreatedDate(), consentRepresentation2.getCreatedDate()); + assertTrue(consentRepresentation2.getLastUpdatedDate() > 0); + assertTrue(consentRepresentation2.getLastUpdatedDate() > consentRepresentation.getLastUpdatedDate()); + assertEquals(1, consentRepresentation2.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation2.getScopes().get(0).getId()); + } + + @Test + public void createConsentForNotExistingClientWithPut() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "not-existing"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + SimpleHttp.Response response = SimpleHttp + .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asResponse(); + + assertEquals(404, response.getStatus()); + } + + @Test + public void createConsentForClientWithoutPermissionWithPut() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + SimpleHttp.Response response = SimpleHttp + .doPut(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asResponse(); + + assertEquals(403, response.getStatus()); + } + + @Test + public void getConsentForClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation1 = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation1.getCreatedDate() > 0); + assertTrue(consentRepresentation1.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation1.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation1.getScopes().get(0).getId()); + + ConsentRepresentation consentRepresentation2 = SimpleHttp + .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertEquals(consentRepresentation1.getLastUpdatedDate(), consentRepresentation2.getLastUpdatedDate()); + assertEquals(consentRepresentation1.getCreatedDate(), consentRepresentation2.getCreatedDate()); + assertEquals(consentRepresentation1.getScopes().get(0).getId(), consentRepresentation2.getScopes().get(0).getId()); + } + + @Test + public void getConsentForNotExistingClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-consent-access", "password"); + String appId = "not-existing"; + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(404, response.getStatus()); + } + + @Test + public void getNotExistingConsentForClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-consent-access", "password"); + String appId = "security-admin-console"; + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(204, response.getStatus()); + } + + @Test + public void getConsentWithoutPermission() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-applications-access", "password"); + String appId = "security-admin-console"; + SimpleHttp.Response response = SimpleHttp + .doGet(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(403, response.getStatus()); + } + + @Test + public void deleteConsentForClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "security-admin-console"; + + ClientScopeRepresentation clientScopeRepresentation = testRealm().clientScopes().findAll().get(0); + ConsentScopeRepresentation consentScopeRepresentation = new ConsentScopeRepresentation(); + consentScopeRepresentation.setId(clientScopeRepresentation.getId()); + + ConsentRepresentation requestedConsent = new ConsentRepresentation(); + requestedConsent.setScopes(Collections.singletonList(consentScopeRepresentation)); + + ConsentRepresentation consentRepresentation = SimpleHttp + .doPost(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .json(requestedConsent) + .auth(token.getToken()) + .asJson(ConsentRepresentation.class); + assertTrue(consentRepresentation.getCreatedDate() > 0); + assertTrue(consentRepresentation.getLastUpdatedDate() > 0); + assertEquals(1, consentRepresentation.getScopes().size()); + assertEquals(consentScopeRepresentation.getId(), consentRepresentation.getScopes().get(0).getId()); + + SimpleHttp.Response response = SimpleHttp + .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(202, response.getStatus()); + + response = SimpleHttp + .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(202, response.getStatus()); + } + + @Test + public void deleteConsentForNotExistingClient() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("manage-consent-access", "password"); + String appId = "not-existing"; + SimpleHttp.Response response = SimpleHttp + .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(404, response.getStatus()); + } + + + @Test + public void deleteConsentWithoutPermission() throws IOException { + assumeFeatureEnabled(ACCOUNT_API); + + TokenUtil token = new TokenUtil("view-consent-access", "password"); + String appId = "security-admin-console"; + SimpleHttp.Response response = SimpleHttp + .doDelete(getAccountUrl("applications/" + appId + "/consent"), httpClient) + .header("Accept", "application/json") + .auth(token.getToken()) + .asResponse(); + assertEquals(403, response.getStatus()); + } }