From 236f3ab76818ebfda9cfef0d8844723eb02d755c Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Fri, 23 May 2014 09:37:07 -0400 Subject: [PATCH] admin cors --- .../src/main/webapp/index.html | 18 +++++ .../src/main/webapp/js/app.js | 20 +++++ .../src/main/webapp/keycloak.json | 2 +- examples/cors/cors-realm.json | 21 +++++- .../org/keycloak/services/managers/Auth.java | 9 +++ .../managers/AuthenticationManager.java | 10 ++- .../services/managers/RealmManager.java | 15 ++-- .../org/keycloak/services/resources/Cors.java | 74 +++++++++++++++---- .../services/resources/admin/AdminRoot.java | 21 +++++- .../testsuite/account/AccountTest.java | 2 - 10 files changed, 165 insertions(+), 27 deletions(-) diff --git a/examples/cors/angular-product-app/src/main/webapp/index.html b/examples/cors/angular-product-app/src/main/webapp/index.html index ebd3968159..2e05d7f92e 100755 --- a/examples/cors/angular-product-app/src/main/webapp/index.html +++ b/examples/cors/angular-product-app/src/main/webapp/index.html @@ -34,6 +34,24 @@ +
+

Realm Roles

+ + + + + + + + + + + + + + +
Role Listing
{{r.name}}
+
diff --git a/examples/cors/angular-product-app/src/main/webapp/js/app.js b/examples/cors/angular-product-app/src/main/webapp/js/app.js index a9d879ed9a..50b68bc3e4 100755 --- a/examples/cors/angular-product-app/src/main/webapp/js/app.js +++ b/examples/cors/angular-product-app/src/main/webapp/js/app.js @@ -29,12 +29,32 @@ angular.element(document).ready(function ($http) { module.controller('GlobalCtrl', function($scope, $http) { $scope.products = []; + $scope.roles = []; $scope.reloadData = function() { $http.get("http://localhost-db:8080/database/products").success(function(data) { $scope.products = angular.fromJson(data); }); + }; + $scope.loadRoles = function() { + $http.query("http://localhost-auth:8080/auth/admin/realms/" + keycloakAuth.realm + "/roles").success(function(data) { + $scope.roles = angular.fromJson(data); + + }); + + }; + $scope.addRole = function() { + $http.post("http://localhost-auth:8080/auth/admin/realms/" + keycloakAuth.realm + "/roles", {name: 'stuff'}).success(function() { + $scope.loadRoles(); + }); + + }; + $scope.deleteRole = function() { + $http.delete("http://localhost-auth:8080/auth/admin/realms/" + keycloakAuth.realm + "/roles/stuff").success(function() { + $scope.loadRoles(); + }); + }; $scope.logout = logout; }); diff --git a/examples/cors/angular-product-app/src/main/webapp/keycloak.json b/examples/cors/angular-product-app/src/main/webapp/keycloak.json index 7ea2954c06..6b94b27755 100755 --- a/examples/cors/angular-product-app/src/main/webapp/keycloak.json +++ b/examples/cors/angular-product-app/src/main/webapp/keycloak.json @@ -1,5 +1,5 @@ { - "realm" : "cors-realm", + "realm" : "cors", "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost-auth:8080/auth", "ssl-not-required" : true, diff --git a/examples/cors/cors-realm.json b/examples/cors/cors-realm.json index 32705aaff0..9b3da42c0a 100755 --- a/examples/cors/cors-realm.json +++ b/examples/cors/cors-realm.json @@ -1,5 +1,5 @@ { - "realm": "cors-realm", + "realm": "cors", "enabled": true, "accessTokenLifespan": 3000, "accessCodeLifespan": 10, @@ -57,5 +57,22 @@ "http://localhost:8080" ] } - ] + ], + "applicationRoleMappings": { + "realm-management": [ + { + "username": "bburke@redhat.com", + "roles": ["realm-admin"] + } + ] + }, + "applicationScopeMappings": { + "realm-management": [ + { + "client": "angular-product", + "roles": ["realm-admin"] + } + ] + } + } diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java index 1966f437ab..d549aa2f5f 100755 --- a/services/src/main/java/org/keycloak/services/managers/Auth.java +++ b/services/src/main/java/org/keycloak/services/managers/Auth.java @@ -35,6 +35,15 @@ public class Auth { this.client = client; } + public Auth(RealmModel realm, AccessToken token, UserModel user, ClientModel client) { + this.cookie = false; + this.token = token; + this.realm = realm; + + this.user = user; + this.client = client; + } + public boolean isCookie() { return cookie; } diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index c23858da48..513d9c0eb4 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -221,7 +221,7 @@ public class AuthenticationManager { return null; } - return new AuthResult(user, session); + return new AuthResult(user, session, token); } catch (VerificationException e) { logger.info("Failed to verify identity token", e); } @@ -361,10 +361,12 @@ public class AuthenticationManager { public class AuthResult { private final UserModel user; private final UserSessionModel session; + private final AccessToken token; - public AuthResult(UserModel user, UserSessionModel session) { + public AuthResult(UserModel user, UserSessionModel session, AccessToken token) { this.user = user; this.session = session; + this.token = token; } public UserSessionModel getSession() { @@ -374,6 +376,10 @@ public class AuthenticationManager { public UserModel getUser() { return user; } + + public AccessToken getToken() { + return token; + } } } diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index 1543087fd8..66a49e0a6c 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -103,7 +103,8 @@ public class RealmManager { } protected void setupAdminConsole(RealmModel realm) { - ApplicationModel adminConsole = new ApplicationManager(this).createApplication(realm, Constants.ADMIN_CONSOLE_APPLICATION); + ApplicationModel adminConsole = realm.getApplicationByName(Constants.ADMIN_CONSOLE_APPLICATION); + if (adminConsole == null) adminConsole = new ApplicationManager(this).createApplication(realm, Constants.ADMIN_CONSOLE_APPLICATION); String baseUrl = contextPath + "/admin/" + realm.getName() + "/console"; adminConsole.setBaseUrl(baseUrl + "/index.html"); adminConsole.setEnabled(true); @@ -113,12 +114,10 @@ public class RealmManager { RoleModel adminRole; if (realm.getName().equals(Config.getAdminRealm())) { adminRole = realm.getRole(AdminRoles.ADMIN); + realm.addScopeMapping(adminConsole, adminRole); } else { - ApplicationModel realmApp = realm.getApplicationByName(getRealmAdminApplicationName(realm)); - adminRole = realmApp.getRole(AdminRoles.REALM_ADMIN); - + // security roles are defined in application for the realm. } - realm.addScopeMapping(adminConsole, adminRole); } public String getMasterRealmAdminApplicationName(RealmModel realm) { @@ -265,7 +264,11 @@ public class RealmManager { ApplicationManager applicationManager = new ApplicationManager(new RealmManager(identitySession)); - ApplicationModel realmAdminApp = applicationManager.createApplication(realm, getRealmAdminApplicationName(realm)); + String realmAdminApplicationName = getRealmAdminApplicationName(realm); + ApplicationModel realmAdminApp = realm.getApplicationByName(realmAdminApplicationName); + if (realmAdminApp == null) { + realmAdminApp = applicationManager.createApplication(realm, realmAdminApplicationName); + } RoleModel adminRole = realmAdminApp.addRole(AdminRoles.REALM_ADMIN); realmAdminApp.setBearerOnly(true); diff --git a/services/src/main/java/org/keycloak/services/resources/Cors.java b/services/src/main/java/org/keycloak/services/resources/Cors.java index 9dbcd39188..dcf34995d5 100755 --- a/services/src/main/java/org/keycloak/services/resources/Cors.java +++ b/services/src/main/java/org/keycloak/services/resources/Cors.java @@ -9,7 +9,9 @@ import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.models.ClientModel; +import org.keycloak.representations.AccessToken; import org.keycloak.util.CollectionUtil; /** @@ -33,7 +35,7 @@ public class Cors { private HttpRequest request; - private ResponseBuilder response; + private ResponseBuilder builder; private Set allowedOrigins; private Set allowedMethods; private Set exposedHeaders; @@ -43,13 +45,21 @@ public class Cors { public Cors(HttpRequest request, ResponseBuilder response) { this.request = request; - this.response = response; + this.builder = response; + } + + public Cors(HttpRequest request) { + this.request = request; } public static Cors add(HttpRequest request, ResponseBuilder response) { return new Cors(request, response); } + public static Cors add(HttpRequest request) { + return new Cors(request); + } + public Cors preflight() { preflight = true; return this; @@ -67,6 +77,13 @@ public class Cors { return this; } + public Cors allowedOrigins(AccessToken token) { + if (token != null) { + allowedOrigins = token.getAllowedOrigins(); + } + return this; + } + public Cors allowedMethods(String... allowedMethods) { this.allowedMethods = new HashSet(Arrays.asList(allowedMethods)); return this; @@ -80,35 +97,66 @@ public class Cors { public Response build() { String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); if (origin == null) { - return response.build(); + return builder.build(); } if (!preflight && (allowedOrigins == null || !allowedOrigins.contains(origin))) { - return response.build(); + return builder.build(); } - response.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + builder.header(ACCESS_CONTROL_ALLOW_ORIGIN, origin); if (allowedMethods != null) { - response.header(ACCESS_CONTROL_ALLOW_METHODS, CollectionUtil.join(allowedMethods)); + builder.header(ACCESS_CONTROL_ALLOW_METHODS, CollectionUtil.join(allowedMethods)); } else { - response.header(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ALLOW_METHODS); + builder.header(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ALLOW_METHODS); } if (exposedHeaders != null) { - response.header(ACCESS_CONTROL_EXPOSE_HEADERS, CollectionUtil.join(exposedHeaders)); + builder.header(ACCESS_CONTROL_EXPOSE_HEADERS, CollectionUtil.join(exposedHeaders)); } - response.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(auth)); + builder.header(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(auth)); if (auth) { - response.header(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER)); + builder.header(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER)); } else { - response.header(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS); + builder.header(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS); } - response.header(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE); + builder.header(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE); - return response.build(); + return builder.build(); + } + public void build(HttpResponse response) { + String origin = request.getHttpHeaders().getRequestHeaders().getFirst(ORIGIN_HEADER); + if (origin == null) { + return; + } + + if (!preflight && (allowedOrigins == null || !allowedOrigins.contains(origin))) { + return; + } + + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + + if (allowedMethods != null) { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_METHODS, CollectionUtil.join(allowedMethods)); + } else { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_METHODS, DEFAULT_ALLOW_METHODS); + } + + if (exposedHeaders != null) { + response.getOutputHeaders().add(ACCESS_CONTROL_EXPOSE_HEADERS, CollectionUtil.join(exposedHeaders)); + } + + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.toString(auth)); + if (auth) { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_HEADERS, String.format("%s, %s", DEFAULT_ALLOW_HEADERS, AUTHORIZATION_HEADER)); + } else { + response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_HEADERS, DEFAULT_ALLOW_HEADERS); + } + + response.getOutputHeaders().add(ACCESS_CONTROL_MAX_AGE, DEFAULT_MAX_AGE); } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java index 1c01d84852..43fecc806a 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java @@ -1,6 +1,9 @@ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; +import org.jboss.resteasy.spi.DefaultOptionsMethodException; +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.UnauthorizedException; @@ -17,10 +20,12 @@ import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; +import org.keycloak.services.resources.Cors; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; @@ -39,6 +44,12 @@ public class AdminRoot { @Context protected UriInfo uriInfo; + @Context + protected HttpRequest request; + + @Context + protected HttpResponse response; + protected AppAuthManager authManager; protected TokenManager tokenManager; @@ -127,7 +138,7 @@ public class AdminRoot { if (consoleApp == null) { throw new NotFoundException("Could not find admin console application"); } - Auth auth = new Auth(realm, authResult.getUser(), consoleApp); + Auth auth = new Auth(realm, token, authResult.getUser(), consoleApp); return auth; @@ -143,10 +154,18 @@ public class AdminRoot { @Path("realms") public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) { + if (request.getHttpMethod().equalsIgnoreCase("OPTIONS")) { + Response response = Cors.add(request, Response.ok()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); + throw new WebApplicationException(response); + } + Auth auth = authenticateRealmAdminRequest(headers); if (auth != null) { logger.info("authenticated admin access for: " + auth.getUser().getLoginName()); } + + Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); + RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager); ResteasyProviderFactory.getInstance().injectProperties(adminResource); //resourceContext.initResource(adminResource); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index 7bec7a7b74..7c8705b8d9 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -158,12 +158,10 @@ public class AccountTest { }); } - /* @Test public void forever() throws Exception{ while (true) Thread.sleep(5000); } - */ @Test public void returnToAppFromQueryParam() {