KEYCLOAK-15332 Missing CORS headers in some endpoints in Account REST API
This commit is contained in:
parent
540516c6a9
commit
bb7ce62cd5
8 changed files with 99 additions and 84 deletions
|
@ -10,8 +10,12 @@ import org.keycloak.credential.CredentialProvider;
|
||||||
import org.keycloak.credential.CredentialTypeMetadata;
|
import org.keycloak.credential.CredentialTypeMetadata;
|
||||||
import org.keycloak.credential.CredentialTypeMetadataContext;
|
import org.keycloak.credential.CredentialTypeMetadataContext;
|
||||||
import org.keycloak.credential.UserCredentialStoreManager;
|
import org.keycloak.credential.UserCredentialStoreManager;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.models.AccountRoles;
|
||||||
import org.keycloak.models.*;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.utils.ModelToRepresentation;
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
|
|
|
@ -18,6 +18,7 @@ package org.keycloak.services.resources.account;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
import org.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.common.enums.AccountRestApiVersion;
|
import org.keycloak.common.enums.AccountRestApiVersion;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
@ -28,6 +29,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.services.managers.AppAuthManager;
|
import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
import org.keycloak.services.resources.Cors;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
|
|
||||||
import javax.ws.rs.HttpMethod;
|
import javax.ws.rs.HttpMethod;
|
||||||
|
@ -37,6 +39,7 @@ import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.HttpHeaders;
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
|
@ -51,6 +54,11 @@ public class AccountLoader {
|
||||||
private KeycloakSession session;
|
private KeycloakSession session;
|
||||||
private EventBuilder event;
|
private EventBuilder event;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
private HttpRequest request;
|
||||||
|
@Context
|
||||||
|
private HttpResponse response;
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(AccountLoader.class);
|
private static final Logger logger = Logger.getLogger(AccountLoader.class);
|
||||||
|
|
||||||
public AccountLoader(KeycloakSession session, EventBuilder event) {
|
public AccountLoader(KeycloakSession session, EventBuilder event) {
|
||||||
|
@ -94,6 +102,9 @@ public class AccountLoader {
|
||||||
@Path("{version : v\\d[0-9a-zA-Z_\\-]*}")
|
@Path("{version : v\\d[0-9a-zA-Z_\\-]*}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Object getVersionedAccountRestService(final @PathParam("version") String version) {
|
public Object getVersionedAccountRestService(final @PathParam("version") String version) {
|
||||||
|
if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {
|
||||||
|
return new CorsPreflightService(request);
|
||||||
|
}
|
||||||
return getAccountRestService(getAccountManagementClient(session.getContext().getRealm()), version);
|
return getAccountRestService(getAccountManagementClient(session.getContext().getRealm()), version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,6 +132,9 @@ public class AccountLoader {
|
||||||
if (authResult == null) {
|
if (authResult == null) {
|
||||||
throw new NotAuthorizedException("Bearer token required");
|
throw new NotAuthorizedException("Bearer token required");
|
||||||
}
|
}
|
||||||
|
Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
|
||||||
|
|
||||||
|
Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response);
|
||||||
|
|
||||||
if (authResult.getUser().getServiceAccountClientLink() != null) {
|
if (authResult.getUser().getServiceAccountClientLink() != null) {
|
||||||
throw new NotAuthorizedException("Service accounts are not allowed to access this service");
|
throw new NotAuthorizedException("Service accounts are not allowed to access this service");
|
||||||
|
@ -137,7 +151,6 @@ public class AccountLoader {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Auth auth = new Auth(session.getContext().getRealm(), authResult.getToken(), authResult.getUser(), client, authResult.getSession(), false);
|
|
||||||
AccountRestService accountRestService = new AccountRestService(session, auth, client, event, version);
|
AccountRestService accountRestService = new AccountRestService(session, auth, client, event, version);
|
||||||
ResteasyProviderFactory.getInstance().injectProperties(accountRestService);
|
ResteasyProviderFactory.getInstance().injectProperties(accountRestService);
|
||||||
accountRestService.init();
|
accountRestService.init();
|
||||||
|
|
|
@ -19,8 +19,8 @@ package org.keycloak.services.resources.account;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
import org.jboss.resteasy.annotations.cache.NoCache;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.enums.AccountRestApiVersion;
|
|
||||||
import org.keycloak.common.Profile;
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.enums.AccountRestApiVersion;
|
||||||
import org.keycloak.common.util.StringPropertyReplacer;
|
import org.keycloak.common.util.StringPropertyReplacer;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
import org.keycloak.events.EventStoreProvider;
|
import org.keycloak.events.EventStoreProvider;
|
||||||
|
@ -42,7 +42,6 @@ import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.Cors;
|
|
||||||
import org.keycloak.services.resources.account.resources.ResourcesService;
|
import org.keycloak.services.resources.account.resources.ResourcesService;
|
||||||
import org.keycloak.services.util.ResolveRelative;
|
import org.keycloak.services.util.ResolveRelative;
|
||||||
import org.keycloak.storage.ReadOnlyException;
|
import org.keycloak.storage.ReadOnlyException;
|
||||||
|
@ -50,16 +49,15 @@ import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
|
import org.keycloak.userprofile.LegacyUserProfileProviderFactory;
|
||||||
import org.keycloak.userprofile.UserProfile;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
|
||||||
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
|
|
||||||
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
|
import org.keycloak.userprofile.profile.DefaultUserProfileContext;
|
||||||
|
import org.keycloak.userprofile.profile.representations.AccountUserRepresentationUserProfile;
|
||||||
|
import org.keycloak.userprofile.utils.UserUpdateHelper;
|
||||||
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
import org.keycloak.userprofile.validation.UserProfileValidationResult;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.NotFoundException;
|
import javax.ws.rs.NotFoundException;
|
||||||
import javax.ws.rs.OPTIONS;
|
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
@ -71,7 +69,6 @@ import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -120,18 +117,6 @@ public class AccountRestService {
|
||||||
eventStore = session.getProvider(EventStoreProvider.class);
|
eventStore = session.getProvider(EventStoreProvider.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* CORS preflight
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
@Path("/")
|
|
||||||
@OPTIONS
|
|
||||||
@NoCache
|
|
||||||
public Response preflight() {
|
|
||||||
return Cors.add(request, Response.ok()).auth().preflight().build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get account information.
|
* Get account information.
|
||||||
*
|
*
|
||||||
|
@ -141,7 +126,7 @@ public class AccountRestService {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response account() {
|
public UserRepresentation account() {
|
||||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE);
|
||||||
|
|
||||||
UserModel user = auth.getUser();
|
UserModel user = auth.getUser();
|
||||||
|
@ -161,7 +146,7 @@ public class AccountRestService {
|
||||||
copiedAttributes.remove(UserModel.USERNAME);
|
copiedAttributes.remove(UserModel.USERNAME);
|
||||||
rep.setAttributes(copiedAttributes);
|
rep.setAttributes(copiedAttributes);
|
||||||
|
|
||||||
return Cors.add(request, Response.ok(rep)).auth().allowedOrigins(auth.getToken()).build();
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Path("/")
|
@Path("/")
|
||||||
|
@ -189,7 +174,7 @@ public class AccountRestService {
|
||||||
UserUpdateHelper.updateAccount(realm, user, updatedUser);
|
UserUpdateHelper.updateAccount(realm, user, updatedUser);
|
||||||
event.success();
|
event.success();
|
||||||
|
|
||||||
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
|
return Response.noContent().build();
|
||||||
} catch (ReadOnlyException e) {
|
} catch (ReadOnlyException e) {
|
||||||
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
@ -270,15 +255,15 @@ public class AccountRestService {
|
||||||
|
|
||||||
ClientModel client = realm.getClientByClientId(clientId);
|
ClientModel client = realm.getClientByClientId(clientId);
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity("No client with clientId: " + clientId + " found.")).build();
|
return ErrorResponse.error("No client with clientId: " + clientId + " found.", Response.Status.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId());
|
UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId());
|
||||||
if (consent == null) {
|
if (consent == null) {
|
||||||
return Cors.add(request, Response.noContent()).build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.ok(modelToRepresentation(consent))).build();
|
return Response.ok(modelToRepresentation(consent)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -299,14 +284,14 @@ public class AccountRestService {
|
||||||
event.event(EventType.REVOKE_GRANT_ERROR);
|
event.event(EventType.REVOKE_GRANT_ERROR);
|
||||||
String msg = String.format("No client with clientId: %s found.", clientId);
|
String msg = String.format("No client with clientId: %s found.", clientId);
|
||||||
event.error(msg);
|
event.error(msg);
|
||||||
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build();
|
return ErrorResponse.error(msg, Response.Status.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
session.users().revokeConsentForClient(realm, user.getId(), client.getId());
|
||||||
new UserSessionManager(session).revokeOfflineToken(user, client);
|
new UserSessionManager(session).revokeOfflineToken(user, client);
|
||||||
event.success();
|
event.success();
|
||||||
|
|
||||||
return Cors.add(request, Response.noContent()).build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -359,7 +344,7 @@ public class AccountRestService {
|
||||||
event.event(EventType.GRANT_CONSENT_ERROR);
|
event.event(EventType.GRANT_CONSENT_ERROR);
|
||||||
String msg = String.format("No client with clientId: %s found.", clientId);
|
String msg = String.format("No client with clientId: %s found.", clientId);
|
||||||
event.error(msg);
|
event.error(msg);
|
||||||
return Cors.add(request, Response.status(Response.Status.NOT_FOUND).entity(msg)).build();
|
return ErrorResponse.error(msg, Response.Status.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -371,9 +356,9 @@ public class AccountRestService {
|
||||||
}
|
}
|
||||||
event.success();
|
event.success();
|
||||||
grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId());
|
grantedConsent = session.users().getConsentByClient(realm, user.getId(), client.getId());
|
||||||
return Cors.add(request, Response.ok(modelToRepresentation(grantedConsent))).build();
|
return Response.ok(modelToRepresentation(grantedConsent)).build();
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
return Cors.add(request, Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage())).build();
|
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -416,7 +401,7 @@ public class AccountRestService {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response applications(@QueryParam("name") String name) {
|
public List<ClientRepresentation> applications(@QueryParam("name") String name) {
|
||||||
checkAccountApiEnabled();
|
checkAccountApiEnabled();
|
||||||
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS);
|
auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_APPLICATIONS);
|
||||||
|
|
||||||
|
@ -461,7 +446,7 @@ public class AccountRestService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.ok(apps)).auth().allowedOrigins(auth.getToken()).build();
|
return apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean matches(ClientModel client, String name) {
|
private boolean matches(ClientModel client, String name) {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -44,7 +45,6 @@ import org.keycloak.representations.account.DeviceRepresentation;
|
||||||
import org.keycloak.representations.account.SessionRepresentation;
|
import org.keycloak.representations.account.SessionRepresentation;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.resources.Cors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -73,9 +73,8 @@ public class SessionResource {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response toRepresentation() {
|
public List<SessionRepresentation> toRepresentation() {
|
||||||
return Cors.add(request, Response.ok(session.sessions().getUserSessions(realm, user).stream()
|
return session.sessions().getUserSessions(realm, user).stream().map(this::toRepresentation).collect(Collectors.toList());
|
||||||
.map(this::toRepresentation).collect(Collectors.toList()))).auth().allowedOrigins(auth.getToken()).build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -87,7 +86,7 @@ public class SessionResource {
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
@NoCache
|
@NoCache
|
||||||
public Response devices() {
|
public Collection<DeviceRepresentation> devices() {
|
||||||
Map<String, DeviceRepresentation> reps = new HashMap<>();
|
Map<String, DeviceRepresentation> reps = new HashMap<>();
|
||||||
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
|
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
|
||||||
|
|
||||||
|
@ -117,7 +116,7 @@ public class SessionResource {
|
||||||
rep.addSession(createSessionRepresentation(s, device));
|
rep.addSession(createSessionRepresentation(s, device));
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.ok(reps.values())).auth().allowedOrigins(auth.getToken()).build();
|
return reps.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,7 +138,7 @@ public class SessionResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,7 +157,7 @@ public class SessionResource {
|
||||||
if (userSession != null && userSession.getUser().equals(user)) {
|
if (userSession != null && userSession.getUser().equals(user)) {
|
||||||
AuthenticationManager.backchannelLogout(session, userSession, true);
|
AuthenticationManager.backchannelLogout(session, userSession, true);
|
||||||
}
|
}
|
||||||
return Cors.add(request, Response.noContent()).auth().allowedOrigins(auth.getToken()).build();
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SessionRepresentation createSessionRepresentation(UserSessionModel s, DeviceRepresentation device) {
|
private SessionRepresentation createSessionRepresentation(UserSessionModel s, DeviceRepresentation device) {
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.account.resources;
|
package org.keycloak.services.resources.account.resources;
|
||||||
|
|
||||||
import javax.ws.rs.core.Response;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
@ -29,7 +28,6 @@ import java.util.stream.Collectors;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.authorization.AuthorizationProvider;
|
import org.keycloak.authorization.AuthorizationProvider;
|
||||||
import org.keycloak.authorization.model.PermissionTicket;
|
import org.keycloak.authorization.model.PermissionTicket;
|
||||||
import org.keycloak.authorization.model.ResourceServer;
|
|
||||||
import org.keycloak.authorization.store.PermissionTicketStore;
|
import org.keycloak.authorization.store.PermissionTicketStore;
|
||||||
import org.keycloak.authorization.store.ResourceStore;
|
import org.keycloak.authorization.store.ResourceStore;
|
||||||
import org.keycloak.authorization.store.ScopeStore;
|
import org.keycloak.authorization.store.ScopeStore;
|
||||||
|
@ -42,7 +40,6 @@ import org.keycloak.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.resources.Cors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
@ -69,10 +66,6 @@ public abstract class AbstractResourceService {
|
||||||
uriInfo = session.getContext().getUri();
|
uriInfo = session.getContext().getUri();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response cors(Response.ResponseBuilder response) {
|
|
||||||
return Cors.add(request, response).auth().allowedOrigins(auth.getToken()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Resource extends ResourceRepresentation {
|
public static class Resource extends ResourceRepresentation {
|
||||||
|
|
||||||
private Client client;
|
private Client client;
|
||||||
|
|
|
@ -66,8 +66,8 @@ public class ResourceService extends AbstractResourceService {
|
||||||
*/
|
*/
|
||||||
@GET
|
@GET
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response getResource() {
|
public Resource getResource() {
|
||||||
return cors(Response.ok(new Resource(resource, provider)));
|
return new Resource(resource, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,7 +78,7 @@ public class ResourceService extends AbstractResourceService {
|
||||||
@GET
|
@GET
|
||||||
@Path("permissions")
|
@Path("permissions")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response toPermissions() {
|
public Collection<Permission> toPermissions() {
|
||||||
Map<String, String> filters = new HashMap<>();
|
Map<String, String> filters = new HashMap<>();
|
||||||
|
|
||||||
filters.put(PermissionTicket.OWNER, user.getId());
|
filters.put(PermissionTicket.OWNER, user.getId());
|
||||||
|
@ -92,7 +92,7 @@ public class ResourceService extends AbstractResourceService {
|
||||||
permissions = resources.iterator().next().getPermissions();
|
permissions = resources.iterator().next().getPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cors(Response.ok(permissions));
|
return permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
|
@ -101,9 +101,9 @@ public class ResourceService extends AbstractResourceService {
|
||||||
public Response user(@QueryParam("value") String value) {
|
public Response user(@QueryParam("value") String value) {
|
||||||
try {
|
try {
|
||||||
final UserModel user = getUser(value);
|
final UserModel user = getUser(value);
|
||||||
return cors(Response.ok(toRepresentation(provider.getKeycloakSession(), provider.getRealm(), user)));
|
return Response.ok(toRepresentation(provider.getKeycloakSession(), provider.getRealm(), user)).build();
|
||||||
} catch (NotFoundException e) {
|
} catch (NotFoundException e) {
|
||||||
return cors(Response.noContent());
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ public class ResourceService extends AbstractResourceService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cors(Response.noContent());
|
return Response.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,7 +183,7 @@ public class ResourceService extends AbstractResourceService {
|
||||||
@GET
|
@GET
|
||||||
@Path("permissions/requests")
|
@Path("permissions/requests")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public Response getPermissionRequests() {
|
public Collection<Permission> getPermissionRequests() {
|
||||||
Map<String, String> filters = new HashMap<>();
|
Map<String, String> filters = new HashMap<>();
|
||||||
|
|
||||||
filters.put(PermissionTicket.OWNER, user.getId());
|
filters.put(PermissionTicket.OWNER, user.getId());
|
||||||
|
@ -196,7 +196,7 @@ public class ResourceService extends AbstractResourceService {
|
||||||
requests.computeIfAbsent(ticket.getRequester(), requester -> new Permission(ticket, provider)).addScope(ticket.getScope().getName());
|
requests.computeIfAbsent(ticket.getRequester(), requester -> new Permission(ticket, provider)).addScope(ticket.getScope().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
return cors(Response.ok(requests.values()));
|
return requests.values();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void grantPermission(UserModel user, String scopeId) {
|
private void grantPermission(UserModel user, String scopeId) {
|
||||||
|
|
|
@ -203,10 +203,10 @@ public class ResourcesService extends AbstractResourceService {
|
||||||
result = result.subList(0, size - 1);
|
result = result.subList(0, size - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cors(Response.ok().entity(result).links(createPageLinks(first, max, size)));
|
return Response.ok().entity(result).links(createPageLinks(first, max, size)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
return cors(Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList())));
|
return Response.ok().entity(query.apply(-1, -1).collect(Collectors.toList())).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Link[] createPageLinks(Integer first, Integer max, int resultSize) {
|
private Link[] createPageLinks(Integer first, Integer max, int resultSize) {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.enums.AccountRestApiVersion;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
@ -34,6 +35,10 @@ import java.io.IOException;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
|
||||||
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
|
@ -67,6 +72,7 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
testRealm.setEditUsernameAllowed(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
|
@ -76,69 +82,84 @@ public class AccountRestServiceCorsTest extends AbstractTestRealmKeycloakTest {
|
||||||
public void testGetProfile() throws IOException, InterruptedException {
|
public void testGetProfile() throws IOException, InterruptedException {
|
||||||
driver.navigate().to(VALID_CORS_URL);
|
driver.navigate().to(VALID_CORS_URL);
|
||||||
|
|
||||||
doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), true);
|
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetProfileInvalidOrigin() throws IOException, InterruptedException {
|
public void testGetProfileInvalidOrigin() throws IOException, InterruptedException {
|
||||||
driver.navigate().to(INVALID_CORS_URL);
|
driver.navigate().to(INVALID_CORS_URL);
|
||||||
|
|
||||||
doJsGet(executor, getAccountUrl(), tokenUtil.getToken(), false);
|
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateProfile() throws IOException {
|
public void testUpdateProfile() throws IOException {
|
||||||
driver.navigate().to(VALID_CORS_URL);
|
driver.navigate().to(VALID_CORS_URL);
|
||||||
|
|
||||||
doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true);
|
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testUpdateProfileInvalidOrigin() throws IOException {
|
public void testUpdateProfileInvalidOrigin() throws IOException {
|
||||||
driver.navigate().to(INVALID_CORS_URL);
|
driver.navigate().to(INVALID_CORS_URL);
|
||||||
|
|
||||||
doJsPost(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false);
|
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"firstName\" : \"Bob\" }", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorResponse() {
|
||||||
|
driver.navigate().to(VALID_CORS_URL);
|
||||||
|
|
||||||
|
Result result = doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"username\" : \"vmuzikar\" }", true);
|
||||||
|
assertEquals(400, result.getStatus());
|
||||||
|
assertThat(result.getResult(), containsString("readOnlyUsernameMessage"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testErrorResponseInvalidOrigin() {
|
||||||
|
driver.navigate().to(INVALID_CORS_URL);
|
||||||
|
|
||||||
|
doXhr(executor, getAccountUrl(), tokenUtil.getToken(), "{ \"username\" : \"vmuzikar\" }", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetVersionedApi() {
|
||||||
|
driver.navigate().to(VALID_CORS_URL);
|
||||||
|
|
||||||
|
doXhr(executor, getAccountUrl() + "/" + AccountRestApiVersion.DEFAULT.getStrVersion(), tokenUtil.getToken(), null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetVersionedApiInvalidOrigin() {
|
||||||
|
driver.navigate().to(INVALID_CORS_URL);
|
||||||
|
|
||||||
|
doXhr(executor, getAccountUrl() + "/" + AccountRestApiVersion.DEFAULT.getStrVersion(), tokenUtil.getToken(), null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getAccountUrl() {
|
private String getAccountUrl() {
|
||||||
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account";
|
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/realms/test/account";
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result doJsGet(JavascriptExecutor executor, String url, String token, boolean expectAllowed) {
|
private Result doXhr(JavascriptExecutor executor, String url, String token, String postData, boolean expectAllowed) {
|
||||||
String js = "var r = new XMLHttpRequest();" +
|
String js = "var r = new XMLHttpRequest();" +
|
||||||
"var r = new XMLHttpRequest();" +
|
"r.open('" + (postData == null ? "GET" : "POST") + "', '" + url + "', false);" +
|
||||||
"r.open('GET', '" + url + "', false);" +
|
|
||||||
"r.setRequestHeader('Accept','application/json');" +
|
|
||||||
"r.setRequestHeader('Authorization','bearer " + token + "');" +
|
|
||||||
"r.send();" +
|
|
||||||
"return r.status + ':::' + r.responseText";
|
|
||||||
return doXhr(executor, js, expectAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result doJsPost(JavascriptExecutor executor, String url, String token, String data, boolean expectAllowed) {
|
|
||||||
String js = "var r = new XMLHttpRequest();" +
|
|
||||||
"var r = new XMLHttpRequest();" +
|
|
||||||
"r.open('POST', '" + url + "', false);" +
|
|
||||||
"r.setRequestHeader('Accept','application/json');" +
|
"r.setRequestHeader('Accept','application/json');" +
|
||||||
"r.setRequestHeader('Content-Type','application/json');" +
|
"r.setRequestHeader('Content-Type','application/json');" +
|
||||||
"r.setRequestHeader('Authorization','bearer " + token + "');" +
|
"r.setRequestHeader('Authorization','bearer " + token + "');" +
|
||||||
"r.send('" + data + "');" +
|
"r.send(" + (postData == null ? "" : "'" + postData + "'") + ");" +
|
||||||
"return r.status + ':::' + r.responseText";
|
"return r.status + ':::' + r.responseText";
|
||||||
return doXhr(executor, js, expectAllowed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Result doXhr(JavascriptExecutor executor, String js, boolean expectAllowed) {
|
|
||||||
Result result = null;
|
Result result = null;
|
||||||
Throwable error = null;
|
Throwable error = null;
|
||||||
try {
|
try {
|
||||||
String response = (String) executor.executeScript(js);
|
String response = (String) executor.executeScript(js);
|
||||||
String r[] = response.split(":::");
|
String[] r = response.split(":::");
|
||||||
result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null);
|
result = new Result(Integer.parseInt(r[0]), r.length == 2 ? r[1] : null);
|
||||||
} catch (Throwable t ) {
|
} catch (Throwable t ) {
|
||||||
error = t;
|
error = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result == null || (result.getStatus() != 200 && result.getStatus() != 204) || error != null) {
|
if (error != null) {
|
||||||
if (expectAllowed) {
|
if (expectAllowed) {
|
||||||
throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
|
throw new AssertionError("Cors request failed: " + WebDriverLogDumper.dumpBrowserLogs(driver));
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in a new issue