From 9a739360028cf3c8704a8fe56402b00dc95f05f6 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 21 Feb 2014 17:46:30 +0000 Subject: [PATCH] KEYCLOAK-292 Fine-grained admin control --- .../resources/admin/js/controllers/realm.js | 23 +- .../resources/admin/partials/menu.html | 2 +- .../admin/partials/realm-detail.html | 170 +++++++------ .../resources/admin/partials/realm-menu.html | 11 +- .../admin/templates/kc-navigation.html | 16 +- .../org/keycloak/models/AccountRoles.java | 13 + .../java/org/keycloak/models/AdminRoles.java | 23 ++ .../java/org/keycloak/models/Constants.java | 8 +- .../org/keycloak/models/KeycloakSession.java | 2 +- .../models/jpa/JpaKeycloakSession.java | 2 +- .../adapters/MongoKeycloakSession.java | 2 +- .../model/test/AbstractModelTest.java | 3 + .../org/keycloak/model/test/AdapterTest.java | 16 +- .../org/keycloak/model/test/ImportTest.java | 7 +- .../services/managers/AppAuthManager.java | 176 +++++++++++++ .../services/managers/ApplianceBootstrap.java | 7 +- .../org/keycloak/services/managers/Auth.java | 138 +++++++++++ .../managers/AuthenticationManager.java | 149 +---------- .../managers/ModelToRepresentation.java | 2 +- .../services/managers/RealmManager.java | 60 ++++- .../services/resources/AccountService.java | 130 ++-------- .../services/resources/RealmsResource.java | 2 +- .../resources/admin/AdminService.java | 233 ++++++++---------- .../resources/admin/RealmAdminResource.java | 40 ++- .../resources/admin/RealmsAdminResource.java | 38 ++- .../resources/admin/UsersResource.java | 5 +- .../keycloak/testutils/KeycloakServer.java | 2 +- .../org/keycloak/testsuite/OAuthClient.java | 14 -- .../testsuite/account/ProfileTest.java | 13 +- .../composites/CompositeImportRoleTest.java | 10 +- .../composites/CompositeRoleTest.java | 10 +- .../keycloak/testsuite/forms/AccountTest.java | 7 +- .../testsuite/oauth/AccessTokenTest.java | 7 +- .../testsuite/rule/AbstractKeycloakRule.java | 20 ++ .../testsuite/social/SocialLoginTest.java | 9 +- 35 files changed, 781 insertions(+), 589 deletions(-) create mode 100644 model/api/src/main/java/org/keycloak/models/AccountRoles.java create mode 100644 model/api/src/main/java/org/keycloak/models/AdminRoles.java create mode 100644 services/src/main/java/org/keycloak/services/managers/AppAuthManager.java create mode 100644 services/src/main/java/org/keycloak/services/managers/Auth.java diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js index f1a4f49422..782069a7ca 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js +++ b/admin-ui/src/main/resources/META-INF/resources/admin/js/controllers/realm.js @@ -7,6 +7,17 @@ module.controller('GlobalCtrl', function($scope, $http, Auth, Current, $location $http.get('/auth/rest/admin/whoami').success(function(data, status) { Auth.user = data; Auth.loggedIn = true; + + Auth.hasAccess = function(realm, role) { + var realmAccess = Auth.user['realm_access']; + if (realmAccess) { + realmAccess = realmAccess[realm]; + if (realmAccess) { + return realmAccess.indexOf(role) >= 0; + } + } + return false; + } }) .error(function(data, status) { Auth.loggedIn = false; @@ -62,7 +73,7 @@ module.controller('RealmDropdownCtrl', function($scope, Realm, Current, Auth, $l } }); -module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $http, $location, Dialog, Notifications) { +module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $http, $location, Dialog, Notifications, Auth) { console.log('RealmCreateCtrl'); Current.realm = null; @@ -131,8 +142,14 @@ module.controller('RealmCreateCtrl', function($scope, Current, Realm, $upload, $ Current.realm = Current.realms[i]; } } - $location.url("/realms/" + realmCopy.realm); - Notifications.success("The realm has been created."); + + $http.get('/auth/rest/admin/whoami').success(function(data, status) { + Auth.user = data; + console.log("reloaded auth"); + + $location.url("/realms/" + realmCopy.realm); + Notifications.success("The realm has been created."); + }); }); }); }; diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html index 2aeab7cb96..275cdaed89 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/menu.html @@ -33,7 +33,7 @@ -
  • +
  • Add Realm
  • diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html index da8375a6fd..d51f280045 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-detail.html @@ -7,98 +7,104 @@
  • Settings
  • General
  • -

    Add Realm

    -

    {{realm.realm}} General Settings

    -

    * Required fields

    -
    -
    - Required Settings -
    - +
    +

    Add Realm

    +

    {{realm.realm}} General Settings

    +

    * Required fields

    + +
    + Required Settings +
    + -
    - +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    -
    - Login Options -
    - -
    - +
    +
    + Login Options +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    -
    - Optional Settings -
    - -
    - +
    +
    + Optional Settings +
    + +
    + +
    - -
    - -
    - +
    + +
    + +
    -
    -
    + + +
    + + +
    +
    + + + +
    +
    + +
    +

    {{realm.realm}}

    +
    -
    - - -
    -
    - - - -
    - - + \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html index 81c9fae06c..6bd704c14d 100755 --- a/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/partials/realm-menu.html @@ -1,10 +1,9 @@ - +
  • Applications
  • +
  • OAuth Clients
  • + \ No newline at end of file diff --git a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html index cfb83b2775..aed507bdfd 100644 --- a/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html +++ b/admin-ui/src/main/resources/META-INF/resources/admin/templates/kc-navigation.html @@ -1,10 +1,10 @@ +
  • Social
  • +
  • Roles
  • +
  • Default Roles
  • +
  • Credentials
  • +
  • Token
  • +
  • Keys
  • +
  • Email
  • + \ No newline at end of file diff --git a/model/api/src/main/java/org/keycloak/models/AccountRoles.java b/model/api/src/main/java/org/keycloak/models/AccountRoles.java new file mode 100644 index 0000000000..824d00a763 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/AccountRoles.java @@ -0,0 +1,13 @@ +package org.keycloak.models; + +/** + * @author Stian Thorgersen + */ +public interface AccountRoles { + + String VIEW_PROFILE = "view-profile"; + String MANAGE_ACCOUNT = "manage-account"; + + String[] ALL = {VIEW_PROFILE, MANAGE_ACCOUNT}; + +} diff --git a/model/api/src/main/java/org/keycloak/models/AdminRoles.java b/model/api/src/main/java/org/keycloak/models/AdminRoles.java new file mode 100644 index 0000000000..ab414bf992 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/AdminRoles.java @@ -0,0 +1,23 @@ +package org.keycloak.models; + +/** + * @author Stian Thorgersen + */ +public class AdminRoles { + + public static String APP_SUFFIX = "-realm"; + + public static String ADMIN = "admin"; + + public static String MANAGE_REALM = "manage-realm"; + public static String MANAGE_USERS = "manage-users"; + public static String MANAGE_APPLICATIONS = "manage-applications"; + public static String MANAGE_CLIENTS = "manage-clients"; + + public static String[] ALL_REALM_ROLES = {MANAGE_REALM, MANAGE_USERS, MANAGE_APPLICATIONS, MANAGE_CLIENTS}; + + public static String getAdminApp(RealmModel realm) { + return realm.getName() + APP_SUFFIX; + } + +} diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java index c2051df421..9dfe1e4972 100755 --- a/model/api/src/main/java/org/keycloak/models/Constants.java +++ b/model/api/src/main/java/org/keycloak/models/Constants.java @@ -5,14 +5,12 @@ package org.keycloak.models; * @version $Revision: 1 $ */ public interface Constants { - String INTERNAL_ROLE = "KEYCLOAK_"; String ADMIN_REALM = "keycloak-admin"; String ADMIN_CONSOLE_APPLICATION = "admin-console"; - String ADMIN_CONSOLE_ADMIN_ROLE = "admin"; + + String INTERNAL_ROLE = "KEYCLOAK_"; String APPLICATION_ROLE = INTERNAL_ROLE + "_APPLICATION"; String IDENTITY_REQUESTER_ROLE = INTERNAL_ROLE + "_IDENTITY_REQUESTER"; - String ACCOUNT_APPLICATION = "account"; - String ACCOUNT_PROFILE_ROLE = "view-profile"; - String ACCOUNT_MANAGE_ROLE = "manage-account"; + String ACCOUNT_MANAGEMENT_APP = "account"; } diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSession.java b/model/api/src/main/java/org/keycloak/models/KeycloakSession.java index f9ed6a2dec..00b4db98d6 100755 --- a/model/api/src/main/java/org/keycloak/models/KeycloakSession.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakSession.java @@ -13,7 +13,7 @@ public interface KeycloakSession { RealmModel createRealm(String id, String name); RealmModel getRealm(String id); RealmModel getRealmByName(String name); - List getRealms(UserModel admin); + List getRealms(); boolean removeRealm(String id); void close(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java index 1a24530d77..78123153e0 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaKeycloakSession.java @@ -49,7 +49,7 @@ public class JpaKeycloakSession implements KeycloakSession { } @Override - public List getRealms(UserModel admin) { + public List getRealms() { TypedQuery query = em.createNamedQuery("getAllRealms", RealmEntity.class); List entities = query.getResultList(); List realms = new ArrayList(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java index a57916b466..1420a09653 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoKeycloakSession.java @@ -67,7 +67,7 @@ public class MongoKeycloakSession implements KeycloakSession { } @Override - public List getRealms(UserModel admin) { + public List getRealms() { DBObject query = new BasicDBObject(); List realms = getMongoStore().loadEntities(RealmEntity.class, query, invocationContext); diff --git a/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java b/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java index c7c9671706..4846b57a46 100644 --- a/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AbstractModelTest.java @@ -13,6 +13,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RoleModel; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.util.JsonSerialization; @@ -34,6 +35,8 @@ public class AbstractModelTest { identitySession = factory.createSession(); identitySession.getTransaction().begin(); realmManager = new RealmManager(identitySession); + + new ApplianceBootstrap().bootstrap(identitySession); } @After diff --git a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java index 6e71d6ed53..8aa12f9b19 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/AdapterTest.java @@ -1,6 +1,7 @@ package org.keycloak.model.test; import org.junit.Assert; +import org.junit.Before; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; @@ -32,12 +33,6 @@ import java.util.Set; public class AdapterTest extends AbstractModelTest { private RealmModel realmModel; - @Test - public void installTest() throws Exception { - new ApplianceBootstrap().bootstrap(identitySession); - - } - @Test public void test1CreateRealm() throws Exception { realmModel = realmManager.createRealm("JUGGLER"); @@ -79,7 +74,6 @@ public class AdapterTest extends AbstractModelTest { realmModel.setUpdateProfileOnInitialSocialLogin(true); realmModel.addDefaultRole("foo"); - System.out.println(realmModel.getId()); realmModel = realmManager.getRealm(realmModel.getId()); Assert.assertNotNull(realmModel); Assert.assertEquals(realmModel.getAccessCodeLifespan(), 100); @@ -93,13 +87,11 @@ public class AdapterTest extends AbstractModelTest { Assert.assertEquals(1, realmModel.getDefaultRoles().size()); Assert.assertEquals("foo", realmModel.getDefaultRoles().get(0)); - String id = realmModel.getId(); - System.out.println("id: " + id); + realmModel.getId(); commit(); - List realms = identitySession.getRealms(null); - System.out.println("num realms: " + realms.size()); - Assert.assertEquals(realms.size(), 1); + List realms = identitySession.getRealms(); + Assert.assertEquals(realms.size(), 2); } diff --git a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java index b44b32fb1a..ba30206ab6 100755 --- a/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java +++ b/model/tests/src/test/java/org/keycloak/model/test/ImportTest.java @@ -4,6 +4,7 @@ import org.junit.Assert; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; +import org.keycloak.models.AccountRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; @@ -56,7 +57,7 @@ public class ImportTest extends AbstractModelTest { // Test applications imported ApplicationModel application = realm.getApplicationByName("Application"); ApplicationModel otherApp = realm.getApplicationByName("OtherApp"); - ApplicationModel accountApp = realm.getApplicationByName(Constants.ACCOUNT_APPLICATION); + ApplicationModel accountApp = realm.getApplicationByName(Constants.ACCOUNT_MANAGEMENT_APP); ApplicationModel nonExisting = realm.getApplicationByName("NonExisting"); Assert.assertNotNull(application); Assert.assertNotNull(otherApp); @@ -80,8 +81,8 @@ public class ImportTest extends AbstractModelTest { Assert.assertTrue(allRoles.contains(realm.getRole("admin"))); Assert.assertTrue(allRoles.contains(application.getRole("app-admin"))); Assert.assertTrue(allRoles.contains(otherApp.getRole("otherapp-admin"))); - Assert.assertTrue(allRoles.contains(accountApp.getRole(Constants.ACCOUNT_PROFILE_ROLE))); - Assert.assertTrue(allRoles.contains(accountApp.getRole(Constants.ACCOUNT_MANAGE_ROLE))); + Assert.assertTrue(allRoles.contains(accountApp.getRole(AccountRoles.VIEW_PROFILE))); + Assert.assertTrue(allRoles.contains(accountApp.getRole(AccountRoles.MANAGE_ACCOUNT))); UserModel wburke = realm.getUser("wburke"); allRoles = realm.getRoleMappings(wburke); diff --git a/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java new file mode 100644 index 0000000000..9aea2f38cd --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/AppAuthManager.java @@ -0,0 +1,176 @@ +package org.keycloak.services.managers; + +import org.jboss.resteasy.logging.Logger; +import org.jboss.resteasy.spi.HttpResponse; +import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.RSATokenVerifier; +import org.keycloak.VerificationException; +import org.keycloak.jose.jws.JWSBuilder; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.AccessToken; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.NewCookie; +import java.net.URI; + +/** + * @author Bill Burke + * @author Stian Thorgersen + */ +public class AppAuthManager extends AuthenticationManager { + protected static Logger logger = Logger.getLogger(AuthenticationManager.class); + + private String cookieName; + private TokenManager tokenManager; + + public AppAuthManager(String cookieName, TokenManager tokenManager) { + this.cookieName = cookieName; + this.tokenManager = tokenManager; + } + + public NewCookie createCookie(RealmModel realm, UserModel client, String code, URI uri) { + JWSInput input = new JWSInput(code); + boolean verifiedCode = false; + try { + verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); + } catch (Exception ignored) { + logger.debug("Failed to verify signature", ignored); + } + if (!verifiedCode) { + logger.debug("unverified access code"); + throw new BadRequestException(); + } + String key = input.readContentAsString(); + AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); + if (accessCode == null) { + logger.debug("bad access code"); + throw new BadRequestException(); + } + if (accessCode.isExpired()) { + logger.debug("access code expired"); + throw new BadRequestException(); + } + if (!accessCode.getToken().isActive()) { + logger.debug("access token expired"); + throw new BadRequestException(); + } + if (!accessCode.getRealm().getId().equals(realm.getId())) { + logger.debug("bad realm"); + throw new BadRequestException(); + + } + if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) { + logger.debug("bad client"); + throw new BadRequestException(); + } + + return createLoginCookie(realm, accessCode.getUser(), accessCode.getClient(), cookieName, uri.getRawPath(), false); + } + + public NewCookie createRefreshCookie(RealmModel realm, UserModel user, UserModel client, URI uri) { + return createLoginCookie(realm, user, client, cookieName, uri.getRawPath(), false); + } + + public void expireCookie(URI uri) { + expireCookie(cookieName, uri.getRawPath()); + } + + public Auth authenticateCookie(RealmModel realm, HttpHeaders headers) { + return authenticateCookie(realm, headers, cookieName, true); + } + + public Auth authenticate(RealmModel realm, HttpHeaders headers) { + Auth auth = authenticateCookie(realm, headers); + if (auth != null) return auth; + return authenticateBearerToken(realm, headers); + } + + private Auth authenticateCookie(RealmModel realm, HttpHeaders headers, String cookieName, boolean checkActive) { + logger.info("authenticateCookie"); + Cookie cookie = headers.getCookies().get(cookieName); + if (cookie == null) { + logger.info("authenticateCookie could not find cookie: {0}", cookieName); + return null; + } + + String tokenString = cookie.getValue(); + try { + AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName(), checkActive); + logger.info("token verified"); + if (checkActive && !token.isActive()) { + logger.info("cookie expired"); + expireCookie(cookie.getName(), cookie.getPath()); + return null; + } + + UserModel user = realm.getUserById(token.getSubject()); + if (user == null || !user.isEnabled()) { + logger.info("Unknown user in cookie"); + expireCookie(cookie.getName(), cookie.getPath()); + return null; + } + + UserModel client = null; + if (token.getIssuedFor() != null) { + client = realm.getUser(token.getIssuedFor()); + if (client == null || !client.isEnabled()) { + logger.info("Unknown client in cookie"); + expireCookie(cookie.getName(), cookie.getPath()); + return null; + } + } + + return new Auth(realm, user, client); + } catch (VerificationException e) { + logger.info("Failed to verify cookie", e); + expireCookie(cookie.getName(), cookie.getPath()); + } + return null; + } + + private Auth authenticateBearerToken(RealmModel realm, HttpHeaders headers) { + String tokenString; + String authHeader = headers.getHeaderString(HttpHeaders.AUTHORIZATION); + if (authHeader == null) { + return null; + } else { + String[] split = authHeader.trim().split("\\s+"); + if (split == null || split.length != 2) throw new NotAuthorizedException("Bearer"); + if (!split[0].equalsIgnoreCase("Bearer")) throw new NotAuthorizedException("Bearer"); + tokenString = split[1]; + } + + try { + AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName()); + if (!token.isActive()) { + throw new NotAuthorizedException("token_expired"); + } + + UserModel user = realm.getUserById(token.getSubject()); + if (user == null || !user.isEnabled()) { + throw new NotAuthorizedException("invalid_user"); + } + + UserModel client = null; + if (token.getIssuedFor() != null) { + client = realm.getUser(token.getIssuedFor()); + if (client == null || !client.isEnabled()) { + throw new NotAuthorizedException("invalid_user"); + } + } + + return new Auth(token, user, client); + } catch (VerificationException e) { + logger.error("Failed to verify token", e); + throw new NotAuthorizedException("invalid_token"); + } + } + +} diff --git a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java index 49ea96a07e..337dcf0e02 100755 --- a/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java +++ b/services/src/main/java/org/keycloak/services/managers/ApplianceBootstrap.java @@ -1,6 +1,7 @@ package org.keycloak.services.managers; import org.jboss.resteasy.logging.Logger; +import org.keycloak.models.AdminRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; @@ -62,7 +63,9 @@ public class ApplianceBootstrap { adminConsole.setBaseUrl("/auth/admin/index.html"); adminConsole.setEnabled(true); - RoleModel adminRole = adminConsole.addRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE); + RoleModel adminRole = realm.getRole(AdminRoles.ADMIN); + + adminConsole.addScope(adminRole); UserModel adminUser = realm.addUser("admin"); adminUser.setEnabled(true); @@ -74,7 +77,7 @@ public class ApplianceBootstrap { realm.grantRole(adminUser, adminRole); - ApplicationModel accountApp = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION); + ApplicationModel accountApp = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APP); for (String r : accountApp.getDefaultRoles()) { realm.grantRole(adminUser, accountApp.getRole(r)); } diff --git a/services/src/main/java/org/keycloak/services/managers/Auth.java b/services/src/main/java/org/keycloak/services/managers/Auth.java new file mode 100644 index 0000000000..3a2b21ce28 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/managers/Auth.java @@ -0,0 +1,138 @@ +package org.keycloak.services.managers; + +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; +import org.keycloak.models.RealmModel; +import org.keycloak.models.RoleModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.AccessToken; + +import javax.ws.rs.ForbiddenException; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** +* @author Stian Thorgersen +*/ +public class Auth { + private final boolean cookie; + private final RealmModel realm; + private final AccessToken token; + private final UserModel user; + private final UserModel client; + + public Auth(RealmModel realm, UserModel user, UserModel client) { + this.cookie = true; + this.realm = realm; + this.token = null; + + this.user = user; + this.client = client; + } + + public Auth(AccessToken token, UserModel user, UserModel client) { + this.cookie = false; + this.token = token; + this.realm = null; + + this.user = user; + this.client = client; + } + + public boolean isCookie() { + return cookie; + } + + public RealmModel getRealm() { + return realm; + } + + public UserModel getUser() { + return user; + } + + public UserModel getClient() { + return client; + } + + public AccessToken getToken() { + return token; + } + + public void require(String role) { + if (!has(role)) { + throw new ForbiddenException(); + } + } + + public void require(String app, String role) { + if (!has(app, role)) { + throw new ForbiddenException(); + } + } + + public void require(ApplicationModel app, String role) { + if (!has(app, role)) { + throw new ForbiddenException(); + } + } + + public void requireOneOf(String app, String... roles) { + if(!hasOneOf(app, roles)) { + throw new ForbiddenException(); + } + } + + public void requireOneOf(ApplicationModel app, String... roles) { + if(!hasOneOf(app, roles)) { + throw new ForbiddenException(); + } + } + + public boolean has(String role) { + if (cookie) { + return realm.hasRole(user, realm.getRole(role)); + } else { + return token.getRealmAccess() != null && token.getRealmAccess().isUserInRole(role); + } + } + + public boolean has(String app, String role) { + if (cookie) { + return realm.hasRole(user, realm.getApplicationByName(app).getRole(role)); + } else { + AccessToken.Access access = token.getResourceAccess(app); + return access != null && access.isUserInRole(role); + } + } + + public boolean has(ApplicationModel app, String role) { + if (cookie) { + return realm.hasRole(user, app.getRole(role)); + } else { + AccessToken.Access access = token.getResourceAccess(app.getName()); + return access != null && access.isUserInRole(role); + } + } + + public boolean hasOneOf(String app, String... roles) { + for (String r : roles) { + if (has(app, r)) { + return true; + } + } + return false; + } + + public boolean hasOneOf(ApplicationModel app, String... roles) { + for (String r : roles) { + if (has(app, r)) { + return true; + } + } + return false; + } +} 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 b82f585c79..90ab901895 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -14,11 +14,8 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.resources.AccountService; -import org.keycloak.services.resources.admin.AdminService; import org.keycloak.services.resources.RealmsResource; -import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; @@ -59,19 +56,6 @@ public class AuthenticationManager { return createLoginCookie(realm, user, null, cookieName, cookiePath, rememberMe); } - public NewCookie createSaasIdentityCookie(RealmModel realm, UserModel user, UriInfo uriInfo) { - String cookieName = AdminService.SAAS_IDENTITY_COOKIE; - URI uri = AdminService.saasCookiePath(uriInfo).build(); - String cookiePath = uri.getRawPath(); - return createLoginCookie(realm, user, null, cookieName, cookiePath, false); - } - - public NewCookie createAccountIdentityCookie(RealmModel realm, UserModel user, UserModel client, URI uri) { - String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE; - String cookiePath = uri.getRawPath(); - return createLoginCookie(realm, user, client, cookieName, cookiePath, false); - } - protected NewCookie createLoginCookie(RealmModel realm, UserModel user, UserModel client, String cookieName, String cookiePath, boolean rememberMe) { AccessToken identityToken = createIdentityToken(realm, user); if (client != null) { @@ -104,7 +88,6 @@ public class AuthenticationManager { return encodedToken; } - public void expireIdentityCookie(RealmModel realm, UriInfo uriInfo) { logger.debug("Expiring identity cookie"); String path = getIdentityCookiePath(realm, uriInfo); @@ -123,17 +106,6 @@ public class AuthenticationManager { return uri.getRawPath(); } - public void expireSaasIdentityCookie(UriInfo uriInfo) { - URI uri = AdminService.saasCookiePath(uriInfo).build(); - String cookiePath = uri.getRawPath(); - expireCookie(AdminService.SAAS_IDENTITY_COOKIE, cookiePath); - } - - public void expireAccountIdentityCookie(URI uri) { - String cookiePath = uri.getRawPath(); - expireCookie(AccountService.ACCOUNT_IDENTITY_COOKIE, cookiePath); - } - public void expireCookie(String cookieName, String path) { HttpResponse response = ResteasyProviderFactory.getContextData(HttpResponse.class); if (response == null) { @@ -149,42 +121,13 @@ public class AuthenticationManager { return authenticateIdentityCookie(realm, uriInfo, headers, true); } - public UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, boolean checkActive) { logger.info("authenticateIdentityCookie"); String cookieName = KEYCLOAK_IDENTITY_COOKIE; - Auth auth = authenticateIdentityCookie(realm, uriInfo, headers, cookieName, checkActive); - return auth != null ? auth.getUser() : null; + return authenticateIdentityCookie(realm, uriInfo, headers, cookieName, checkActive); } - public UserModel authenticateSaasIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { - String cookieName = AdminService.SAAS_IDENTITY_COOKIE; - Auth auth = authenticateIdentityCookie(realm, uriInfo, headers, cookieName, true); - return auth != null ? auth.getUser() : null; - } - - public Auth authenticateAccountIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { - String cookieName = AccountService.ACCOUNT_IDENTITY_COOKIE; - return authenticateIdentityCookie(realm, uriInfo, headers, cookieName, true); - } - - public UserModel authenticateSaasIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { - UserModel user = authenticateSaasIdentityCookie(realm, uriInfo, headers); - if (user != null) return user; - - Auth auth = authenticateBearerToken(realm, headers); - return auth != null ? auth.getUser() : null; - } - - public Auth authenticateAccountIdentity(RealmModel realm, UriInfo uriInfo, HttpHeaders headers) { - Auth auth = authenticateAccountIdentityCookie(realm, uriInfo, headers); - if (auth != null) return auth; - - return authenticateBearerToken(realm, headers); - } - - - protected Auth authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) { + protected UserModel authenticateIdentityCookie(RealmModel realm, UriInfo uriInfo, HttpHeaders headers, String cookieName, boolean checkActive) { logger.info("authenticateIdentityCookie"); Cookie cookie = headers.getCookies().get(cookieName); if (cookie == null) { @@ -202,27 +145,14 @@ public class AuthenticationManager { return null; } - Auth auth = new Auth(token); - UserModel user = realm.getUserById(token.getSubject()); if (user == null || !user.isEnabled()) { logger.info("Unknown user in identity cookie"); expireIdentityCookie(realm, uriInfo); return null; } - auth.setUser(user); - if (token.getIssuedFor() != null) { - UserModel client = realm.getUser(token.getIssuedFor()); - if (client == null || !client.isEnabled()) { - logger.info("Unknown client in identity cookie"); - expireIdentityCookie(realm, uriInfo); - return null; - } - auth.setClient(client); - } - - return auth; + return user; } catch (VerificationException e) { logger.info("Failed to verify identity cookie", e); expireCookie(cookie.getName(), cookie.getPath()); @@ -230,49 +160,6 @@ public class AuthenticationManager { return null; } - public Auth authenticateBearerToken(RealmModel realm, HttpHeaders headers) { - String tokenString = null; - String authHeader = headers.getHeaderString(HttpHeaders.AUTHORIZATION); - if (authHeader == null) { - return null; - } else { - String[] split = authHeader.trim().split("\\s+"); - if (split == null || split.length != 2) throw new NotAuthorizedException("Bearer"); - if (!split[0].equalsIgnoreCase("Bearer")) throw new NotAuthorizedException("Bearer"); - tokenString = split[1]; - } - - - try { - AccessToken token = RSATokenVerifier.verifyToken(tokenString, realm.getPublicKey(), realm.getName()); - if (!token.isActive()) { - throw new NotAuthorizedException("token_expired"); - } - - Auth auth = new Auth(token); - - UserModel user = realm.getUserById(token.getSubject()); - if (user == null || !user.isEnabled()) { - throw new NotAuthorizedException("invalid_user"); - } - auth.setUser(user); - - if (token.getIssuedFor() != null) { - UserModel client = realm.getUser(token.getIssuedFor()); - if (client == null || !client.isEnabled()) { - throw new NotAuthorizedException("invalid_user"); - } - auth.setClient(client); - } - - return auth; - } catch (VerificationException e) { - logger.error("Failed to verify token", e); - throw new NotAuthorizedException("invalid_token"); - } - } - - public AuthenticationStatus authenticateForm(RealmModel realm, UserModel user, MultivaluedMap formData) { if (user == null) { logger.debug("Not Authenticated! Incorrect user name"); @@ -356,34 +243,4 @@ public class AuthenticationManager { SUCCESS, ACCOUNT_DISABLED, ACTIONS_REQUIRED, INVALID_USER, INVALID_CREDENTIALS, MISSING_PASSWORD, MISSING_TOTP, FAILED } - public static class Auth { - private AccessToken token; - private UserModel user; - private UserModel client; - - public Auth(AccessToken token) { - this.token = token; - } - - public AccessToken getToken() { - return token; - } - - public UserModel getUser() { - return user; - } - - public UserModel getClient() { - return client; - } - - void setUser(UserModel user) { - this.user = user; - } - - void setClient(UserModel client) { - this.client = client; - } - } - } diff --git a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java index 9bf9142624..303cd94172 100755 --- a/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java +++ b/services/src/main/java/org/keycloak/services/managers/ModelToRepresentation.java @@ -85,7 +85,7 @@ public class ModelToRepresentation { rep.setPasswordPolicy(realm.getPasswordPolicy().toString()); } - ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION); + ApplicationModel accountManagementApplication = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APP); List defaultRoles = realm.getDefaultRoles(); if (!defaultRoles.isEmpty()) { 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 4828002486..ca71051399 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -1,6 +1,8 @@ package org.keycloak.services.managers; import org.jboss.resteasy.logging.Logger; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.AdminRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; @@ -59,7 +61,6 @@ public class RealmManager { return identitySession.getRealmByName(name); } - public RealmModel createRealm(String name) { return createRealm(name, name); } @@ -71,13 +72,31 @@ public class RealmManager { realm.addRole(Constants.APPLICATION_ROLE); realm.addRole(Constants.IDENTITY_REQUESTER_ROLE); + setupAdminManagement(realm); setupAccountManagement(realm); + realm.addRequiredOAuthClientCredential(UserCredentialModel.SECRET); realm.addRequiredResourceCredential(UserCredentialModel.SECRET); return realm; } + public boolean removeRealm(RealmModel realm) { + boolean removed = identitySession.removeRealm(realm.getId()); + + RealmModel adminRealm = getKeycloakAdminstrationRealm(); + RoleModel adminRole = adminRealm.getRole(AdminRoles.ADMIN); + + ApplicationModel realmAdminApp = adminRealm.getApplicationByName(AdminRoles.getAdminApp(realm)); + for (RoleModel r : realmAdminApp.getRoles()) { + adminRole.removeCompositeRole(r); + } + + adminRealm.removeApplication(realmAdminApp.getId()); + + return removed; + } + public void generateRealmKeys(RealmModel realm) { KeyPair keyPair = null; try { @@ -134,19 +153,41 @@ public class RealmManager { } } - private void setupAccountManagement(RealmModel realm) { - ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION); - if (application == null) { - application = new ApplicationManager(this).createApplication(realm, Constants.ACCOUNT_APPLICATION); - application.setEnabled(true); + private void setupAdminManagement(RealmModel realm) { + RealmModel adminRealm; + RoleModel adminRole; - application.addDefaultRole(Constants.ACCOUNT_PROFILE_ROLE); - application.addDefaultRole(Constants.ACCOUNT_MANAGE_ROLE); + if (realm.getName().equals(Constants.ADMIN_REALM)) { + adminRealm = realm; + adminRole = realm.addRole(AdminRoles.ADMIN); + } else { + adminRealm = identitySession.getRealmByName(Constants.ADMIN_REALM); + adminRole = adminRealm.getRole(AdminRoles.ADMIN); + } + + ApplicationManager applicationManager = new ApplicationManager(new RealmManager(identitySession)); + ApplicationModel realmAdminApp = applicationManager.createApplication(adminRealm, AdminRoles.getAdminApp(realm)); + + for (String r : AdminRoles.ALL_REALM_ROLES) { + RoleModel role = realmAdminApp.addRole(r); + adminRole.addCompositeRole(role); } } - public RealmModel importRealm(RealmRepresentation rep, UserModel realmCreator) { + private void setupAccountManagement(RealmModel realm) { + ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APP); + if (application == null) { + application = new ApplicationManager(this).createApplication(realm, Constants.ACCOUNT_MANAGEMENT_APP); + application.setEnabled(true); + + for (String role : AccountRoles.ALL) { + application.addDefaultRole(role); + } + } + } + + public RealmModel importRealm(RealmRepresentation rep) { String id = rep.getId(); if (id == null) { id = KeycloakModelUtils.generateId(); @@ -299,7 +340,6 @@ public class RealmManager { } - if (rep.getRoleMappings() != null) { for (UserRoleMappingRepresentation mapping : rep.getRoleMappings()) { UserModel user = userMap.get(mapping.getUsername()); diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 12c167ab63..407d2c9b4a 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -23,7 +23,6 @@ package org.keycloak.services.resources; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; -import org.keycloak.AbstractOAuthClient; import org.keycloak.account.Account; import org.keycloak.account.AccountLoader; import org.keycloak.account.AccountPages; @@ -32,10 +31,10 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.*; import org.keycloak.models.utils.TimeBasedOTP; -import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.AccessCodeEntry; -import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.TokenManager; import org.keycloak.services.messages.Messages; @@ -68,23 +67,23 @@ public class AccountService { @Context private UriInfo uriInfo; - private AuthenticationManager authManager = new AuthenticationManager(); + private AppAuthManager authManager; private ApplicationModel application; - private TokenManager tokenManager; - public AccountService(RealmModel realm, ApplicationModel application, TokenManager tokenManager) { this.realm = realm; this.application = application; - this.tokenManager = tokenManager; + this.authManager = new AppAuthManager("KEYCLOAK_ACCOUNT_IDENTITY", tokenManager); } private Response forwardToPage(String path, AccountPages page) { - AuthenticationManager.Auth auth = getAuth(false); + Auth auth = getAuth(false); if (auth != null) { - if (!hasAccess(auth)) { - return noAccess(); + try { + auth.require(application, AccountRoles.MANAGE_ACCOUNT); + } catch (ForbiddenException e) { + return Flows.forms(realm, request, uriInfo).setError("No access").createErrorPage(); } Account account = AccountLoader.load().createAccount(uriInfo).setRealm(realm).setUser(auth.getUser()); @@ -100,10 +99,6 @@ public class AccountService { } } - private Response noAccess() { - return Flows.forms(realm, request, uriInfo).setError("No access").createErrorPage(); - } - @Path("/") @OPTIONS public Response accountPreflight() { @@ -117,10 +112,9 @@ public class AccountService { if (types.contains(MediaType.WILDCARD_TYPE) || (types.contains(MediaType.TEXT_HTML_TYPE))) { return forwardToPage(null, AccountPages.ACCOUNT); } else if (types.contains(MediaType.APPLICATION_JSON_TYPE)) { - AuthenticationManager.Auth auth = getAuth(true); - if (!hasAccess(auth, Constants.ACCOUNT_PROFILE_ROLE)) { - return Response.status(Response.Status.FORBIDDEN).build(); - } + Auth auth = getAuth(true); + auth.requireOneOf(application, AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); + return Cors.add(request, Response.ok(ModelToRepresentation.toRepresentation(auth.getUser()))).auth().allowedOrigins(auth.getClient()).build(); } else { return Response.notAcceptable(Variant.VariantListBuilder.newInstance().mediaTypes(MediaType.TEXT_HTML_TYPE, MediaType.APPLICATION_JSON_TYPE).build()).build(); @@ -143,10 +137,8 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processAccountUpdate(final MultivaluedMap formData) { - AuthenticationManager.Auth auth = getAuth(true); - if (!hasAccess(auth)) { - return noAccess(); - } + Auth auth = getAuth(true); + auth.require(application, AccountRoles.MANAGE_ACCOUNT); UserModel user = auth.getUser(); @@ -167,10 +159,8 @@ public class AccountService { @Path("totp-remove") @GET public Response processTotpRemove() { - AuthenticationManager.Auth auth = getAuth(true); - if (!hasAccess(auth)) { - return noAccess(); - } + Auth auth = getAuth(true); + auth.require(application, AccountRoles.MANAGE_ACCOUNT); UserModel user = auth.getUser(); user.setTotp(false); @@ -183,10 +173,8 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processTotpUpdate(final MultivaluedMap formData) { - AuthenticationManager.Auth auth = getAuth(true); - if (!hasAccess(auth)) { - return noAccess(); - } + Auth auth = getAuth(true); + auth.require(application, AccountRoles.MANAGE_ACCOUNT); UserModel user = auth.getUser(); @@ -215,10 +203,8 @@ public class AccountService { @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response processPasswordUpdate(final MultivaluedMap formData) { - AuthenticationManager.Auth auth = getAuth(true); - if (!hasAccess(auth)) { - return noAccess(); - } + Auth auth = getAuth(true); + auth.require(application, AccountRoles.MANAGE_ACCOUNT); UserModel user = auth.getUser(); @@ -285,61 +271,25 @@ public class AccountService { throw new BadRequestException(); } - JWSInput input = new JWSInput(code); - boolean verifiedCode = false; - try { - verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); - } catch (Exception ignored) { - logger.debug("Failed to verify signature", ignored); - } - if (!verifiedCode) { - logger.debug("unverified access code"); - throw new BadRequestException(); - } - String key = input.readContentAsString(); - AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); - if (accessCode == null) { - logger.debug("bad access code"); - throw new BadRequestException(); - } - if (accessCode.isExpired()) { - logger.debug("access code expired"); - throw new BadRequestException(); - } - if (!accessCode.getToken().isActive()) { - logger.debug("access token expired"); - throw new BadRequestException(); - } - if (!accessCode.getRealm().getId().equals(realm.getId())) { - logger.debug("bad realm"); - throw new BadRequestException(); - - } - if (!client.getLoginName().equals(accessCode.getClient().getLoginName())) { - logger.debug("bad client"); - throw new BadRequestException(); - } - URI accountUri = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()); URI redirectUri = path != null ? accountUri.resolve(path) : accountUri; if (referrer != null) { redirectUri = redirectUri.resolve("?referrer=" + referrer); } - NewCookie cookie = authManager.createAccountIdentityCookie(realm, accessCode.getUser(), client, Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName())); + NewCookie cookie = authManager.createCookie(realm, client, code, Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName())); return Response.status(302).cookie(cookie).location(redirectUri).build(); } finally { - authManager.expireCookie(AbstractOAuthClient.OAUTH_TOKEN_REQUEST_STATE, uriInfo.getAbsolutePath().getRawPath()); + authManager.expireCookie(Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName())); } } @Path("logout") @GET public Response logout() { - // TODO Should use single-sign out via TokenService URI baseUri = Urls.accountBase(uriInfo.getBaseUri()).build(realm.getName()); authManager.expireIdentityCookie(realm, uriInfo); - authManager.expireAccountIdentityCookie(baseUri); + authManager.expireCookie(baseUri); return Response.status(302).location(baseUri).build(); } @@ -348,7 +298,7 @@ public class AccountService { String authUrl = Urls.realmLoginPage(uriInfo.getBaseUri(), realm.getName()).toString(); oauth.setAuthUrl(authUrl); - oauth.setClientId(Constants.ACCOUNT_APPLICATION); + oauth.setClientId(Constants.ACCOUNT_MANAGEMENT_APP); UriBuilder uriBuilder = Urls.accountPageBuilder(uriInfo.getBaseUri()).path(AccountService.class, "loginRedirect"); @@ -368,42 +318,14 @@ public class AccountService { return oauth.redirect(uriInfo, accountUri.toString()); } - private AuthenticationManager.Auth getAuth(boolean error) { - AuthenticationManager.Auth auth = authManager.authenticateAccountIdentity(realm, uriInfo, headers); + private Auth getAuth(boolean error) { + Auth auth = authManager.authenticate(realm, headers); if (auth == null && error) { throw new ForbiddenException(); } return auth; } - private boolean hasAccess(AuthenticationManager.Auth auth) { - return hasAccess(auth, null); - } - - private boolean hasAccess(AuthenticationManager.Auth auth, String role) { - UserModel client = auth.getClient(); - if (realm.hasRole(client, realm.getRole(Constants.APPLICATION_ROLE))) { - // Tokens from cookies don't have roles - UserModel user = auth.getUser(); - if (hasRole(user, Constants.ACCOUNT_MANAGE_ROLE) || (role != null && hasRole(user, role))) { - return true; - } - } - - AccessToken.Access access = auth.getToken().getResourceAccess(application.getName()); - if (access != null) { - if (access.isUserInRole(Constants.ACCOUNT_MANAGE_ROLE) || (role != null && access.isUserInRole(role))) { - return true; - } - } - - return false; - } - - private boolean hasRole(UserModel user, String role) { - return realm.hasRole(user, application.getRole(role)); - } - private String getReferrer() { String referrer = uriInfo.getQueryParameters().getFirst("referrer"); if (referrer != null) { diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 2338387d1b..3a2ea920f1 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -69,7 +69,7 @@ public class RealmsResource { RealmManager realmManager = new RealmManager(session); RealmModel realm = locateRealm(name, realmManager); - ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_APPLICATION); + ApplicationModel application = realm.getApplicationNameMap().get(Constants.ACCOUNT_MANAGEMENT_APP); if (application == null || !application.isEnabled()) { logger.debug("account management not enabled"); throw new NotFoundException(); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java index 86658039b0..7a5719ad7e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminService.java @@ -1,49 +1,44 @@ package org.keycloak.services.resources.admin; +import org.codehaus.jackson.annotate.JsonProperty; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; -import org.jboss.resteasy.spi.NotImplementedYetException; -import org.keycloak.AbstractOAuthClient; import org.keycloak.jaxrs.JaxrsOAuthClient; -import org.keycloak.jose.jws.JWSInput; -import org.keycloak.jose.jws.crypto.RSAProvider; +import org.keycloak.models.AdminRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; import org.keycloak.models.UserModel; -import org.keycloak.services.managers.AccessCodeEntry; -import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.AuthenticationManager.AuthenticationStatus; +import org.keycloak.services.managers.AppAuthManager; +import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; -import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.TokenService; import org.keycloak.services.resources.flows.Flows; -import org.keycloak.services.resources.flows.OAuthFlows; -import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.NotAuthorizedException; import javax.ws.rs.NotFoundException; -import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.NewCookie; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; /** * @author Bill Burke @@ -52,8 +47,6 @@ import java.net.URI; @Path("/admin") public class AdminService { protected static final Logger logger = Logger.getLogger(AdminService.class); - public static final String REALM_CREATOR_ROLE = "realm-creator"; - public static final String SAAS_IDENTITY_COOKIE = "KEYCLOAK_SAAS_IDENTITY"; @Context protected UriInfo uriInfo; @@ -73,25 +66,32 @@ public class AdminService { @Context protected Providers providers; - protected String adminPath = "/admin/index.html"; - protected AuthenticationManager authManager = new AuthenticationManager(); + protected AppAuthManager authManager; protected TokenManager tokenManager; public AdminService(TokenManager tokenManager) { this.tokenManager = tokenManager; + this.authManager = new AppAuthManager("KEYCLOAK_ADMIN_CONSOLE_IDENTITY", tokenManager); } public static class WhoAmI { protected String userId; protected String displayName; + @JsonProperty("admin") + protected boolean admin; + @JsonProperty("realm_access") + protected Map> realmAccess = new HashMap>(); + public WhoAmI() { } - public WhoAmI(String userId, String displayName) { + public WhoAmI(String userId, String displayName, boolean admin, Map> realmAccess) { this.userId = userId; this.displayName = displayName; + this.admin = admin; + this.realmAccess = realmAccess; } public String getUserId() { @@ -109,6 +109,22 @@ public class AdminService { public void setDisplayName(String displayName) { this.displayName = displayName; } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public Map> getRealmAccess() { + return realmAccess; + } + + public void setRealmAccess(Map> realmAccess) { + this.realmAccess = realmAccess; + } } @Path("keepalive") @@ -120,11 +136,11 @@ public class AdminService { RealmModel realm = getAdminstrationRealm(realmManager); if (realm == null) throw new NotFoundException(); - UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers); - if (user == null) { + Auth auth = authManager.authenticateCookie(realm, headers); + if (auth == null) { return Response.status(401).build(); } - NewCookie refreshCookie = authManager.createSaasIdentityCookie(realm, user, uriInfo); + NewCookie refreshCookie = authManager.createRefreshCookie(realm, auth.getUser(), auth.getClient(), AdminService.saasCookiePath(uriInfo).build()); return Response.noContent().cookie(refreshCookie).build(); } @@ -137,15 +153,50 @@ public class AdminService { RealmModel realm = getAdminstrationRealm(realmManager); if (realm == null) throw new NotFoundException(); - UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers); + Auth auth = authManager.authenticateCookie(realm, headers); + UserModel user = auth.getUser(); if (user == null) { return Response.status(401).build(); } - // keycloak is bootstrapped with an admin user with no first/last name, so use login name as display name - return Response.ok(new WhoAmI(user.getLoginName(), user.getLoginName())).build(); + + String displayName; + if (user.getFirstName() != null || user.getLastName() != null) { + displayName = user.getFirstName(); + if (user.getLastName() != null) { + displayName = displayName != null ? displayName + " " + user.getLastName() : user.getLastName(); + } + } else { + displayName = user.getLoginName(); + } + + boolean admin = realm.hasRole(user, realm.getRole("admin")); + + Map> realmAccess = new HashMap>(); + addRealmAdminAccess(realmAccess, auth.getRealm().getRoleMappings(auth.getUser())); + + return Response.ok(new WhoAmI(user.getId(), displayName, admin, realmAccess)).build(); } - @Path("isLoggedIn.js") + private void addRealmAdminAccess(Map> realmAdminAccess, Set roles) { + for (RoleModel r : roles) { + if (r.getContainer() instanceof ApplicationModel) { + ApplicationModel app = (ApplicationModel) r.getContainer(); + if (app.getName().endsWith(AdminRoles.APP_SUFFIX)) { + String realm = app.getName().substring(0, app.getName().length() - AdminRoles.APP_SUFFIX.length()); + if (!realmAdminAccess.containsKey(realm)) { + realmAdminAccess.put(realm, new HashSet()); + } + realmAdminAccess.get(realm).add(r.getName()); + } + } + + if (r.isComposite()) { + addRealmAdminAccess(realmAdminAccess, r.getComposites()); + } + } + } + + @Path("isLoggedIn.js") @GET @Produces("application/javascript") @NoCache @@ -157,7 +208,7 @@ public class AdminService { return "var keycloakCookieLoggedIn = false;"; } - UserModel user = authManager.authenticateSaasIdentityCookie(realm, uriInfo, headers); + UserModel user = authManager.authenticateCookie(realm, headers).getUser(); if (user == null) { return "var keycloakCookieLoggedIn = false;"; } @@ -176,23 +227,14 @@ public class AdminService { @Path("realms") public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) { RealmManager realmManager = new RealmManager(session); - RealmModel saasRealm = getAdminstrationRealm(realmManager); - if (saasRealm == null) + RealmModel adminRealm = getAdminstrationRealm(realmManager); + if (adminRealm == null) throw new NotFoundException(); - UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers); - if (admin == null) { + Auth auth = authManager.authenticate(adminRealm, headers); + if (auth == null) { throw new NotAuthorizedException("Bearer"); } - ApplicationModel adminConsole = saasRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); - if (adminConsole == null) { - throw new NotFoundException(); - } - RoleModel adminRole = adminConsole.getRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE); - if (!saasRealm.hasRole(admin, adminRole)) { - logger.warn("not a Realm admin"); - throw new NotAuthorizedException("Bearer"); - } - RealmsAdminResource adminResource = new RealmsAdminResource(admin, tokenManager); + RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager); resourceContext.initResource(adminResource); return adminResource; } @@ -200,35 +242,33 @@ public class AdminService { @Path("serverinfo") public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) { RealmManager realmManager = new RealmManager(session); - RealmModel saasRealm = getAdminstrationRealm(realmManager); - if (saasRealm == null) + RealmModel adminRealm = getAdminstrationRealm(realmManager); + if (adminRealm == null) throw new NotFoundException(); - UserModel admin = authManager.authenticateSaasIdentity(saasRealm, uriInfo, headers); + Auth auth = authManager.authenticate(adminRealm, headers); + UserModel admin = auth.getUser(); if (admin == null) { throw new NotAuthorizedException("Bearer"); } - ApplicationModel adminConsole = saasRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); + ApplicationModel adminConsole = adminRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); if (adminConsole == null) { throw new NotFoundException(); } - RoleModel adminRole = adminConsole.getRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE); - if (!saasRealm.hasRole(admin, adminRole)) { - logger.warn("not a Realm admin"); - throw new NotAuthorizedException("Bearer"); - } ServerInfoAdminResource adminResource = new ServerInfoAdminResource(); resourceContext.initResource(adminResource); return adminResource; } + private void expireCookie() { + authManager.expireCookie(AdminService.saasCookiePath(uriInfo).build()); + } + @Path("login") @GET @NoCache public Response loginPage(@QueryParam("path") String path) { logger.debug("loginPage ********************** <---"); - RealmManager realmManager = new RealmManager(session); - RealmModel realm = getAdminstrationRealm(realmManager); - authManager.expireSaasIdentityCookie(uriInfo); + expireCookie(); JaxrsOAuthClient oauth = new JaxrsOAuthClient(); String authUrl = TokenService.loginPageUrl(uriInfo).build(Constants.ADMIN_REALM).toString(); @@ -259,7 +299,6 @@ public class AdminService { URI uri = uriInfo.getBaseUriBuilder().path(AdminService.class).path(AdminService.class, "errorOnLoginRedirect").queryParam("error", message).build(); URI logout = TokenService.logoutUrl(uriInfo).queryParam("redirect_uri", uri.toString()).build(Constants.ADMIN_REALM); return Response.status(302).location(logout).build(); - } @Path("login-redirect") @@ -279,12 +318,12 @@ public class AdminService { return redirectOnLoginError(error); } RealmManager realmManager = new RealmManager(session); - RealmModel realm = getAdminstrationRealm(realmManager); - if (!realm.isEnabled()) { + RealmModel adminRealm = getAdminstrationRealm(realmManager); + if (!adminRealm.isEnabled()) { logger.debug("realm not enabled"); return redirectOnLoginError("realm not enabled"); } - ApplicationModel adminConsole = realm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); + ApplicationModel adminConsole = adminRealm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); UserModel adminConsoleUser = adminConsole.getApplicationUser(); if (!adminConsole.isEnabled() || !adminConsoleUser.isEnabled()) { logger.debug("admin app not enabled"); @@ -301,47 +340,8 @@ public class AdminService { } new JaxrsOAuthClient().checkStateCookie(uriInfo, headers); - JWSInput input = new JWSInput(code); - boolean verifiedCode = false; - try { - verifiedCode = RSAProvider.verify(input, realm.getPublicKey()); - } catch (Exception ignored) { - logger.debug("Failed to verify signature", ignored); - } - if (!verifiedCode) { - logger.debug("unverified access code"); - return redirectOnLoginError("invalid login data"); - } - String key = input.readContentAsString(); - AccessCodeEntry accessCode = tokenManager.pullAccessCode(key); - if (accessCode == null) { - logger.debug("bad access code"); - return redirectOnLoginError("invalid login data"); - } - if (accessCode.isExpired()) { - logger.debug("access code expired"); - return redirectOnLoginError("invalid login data"); - } - if (!accessCode.getToken().isActive()) { - logger.debug("access token expired"); - return redirectOnLoginError("invalid login data"); - } - if (!accessCode.getRealm().getId().equals(realm.getId())) { - logger.debug("bad realm"); - return redirectOnLoginError("invalid login data"); - - } - if (!adminConsoleUser.getLoginName().equals(accessCode.getClient().getLoginName())) { - logger.debug("bad client"); - return redirectOnLoginError("invalid login data"); - } - RoleModel adminConsoleAdminRole = adminConsole.getRole(Constants.ADMIN_CONSOLE_ADMIN_ROLE); - if (!realm.hasRole(accessCode.getUser(), adminConsoleAdminRole)) { - logger.debug("not allowed"); - return redirectOnLoginError("No permission to access console"); - } logger.debug("loginRedirect SUCCESS"); - NewCookie cookie = authManager.createSaasIdentityCookie(realm, accessCode.getUser(), uriInfo); + NewCookie cookie = authManager.createCookie(adminRealm, adminConsoleUser, code, AdminService.saasCookiePath(uriInfo).build()); URI redirectUri = contextRoot(uriInfo).path(adminPath).build(); if (path != null) { @@ -349,7 +349,7 @@ public class AdminService { } return Response.status(302).cookie(cookie).location(redirectUri).build(); } finally { - authManager.expireCookie(AbstractOAuthClient.OAUTH_TOKEN_REQUEST_STATE, uriInfo.getAbsolutePath().getPath()); + expireCookie(); } } @@ -359,7 +359,7 @@ public class AdminService { public Response logout() { RealmManager realmManager = new RealmManager(session); RealmModel realm = getAdminstrationRealm(realmManager); - authManager.expireSaasIdentityCookie(uriInfo); + expireCookie(); authManager.expireIdentityCookie(realm, uriInfo); return Response.status(302).location(uriInfo.getBaseUriBuilder().path(AdminService.class).path(AdminService.class, "loginPage").build()).build(); @@ -370,42 +370,7 @@ public class AdminService { @NoCache public void logoutCookie() { logger.debug("*** logoutCookie"); - authManager.expireSaasIdentityCookie(uriInfo); - } - - @Path("login") - @POST - @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response processLogin(final MultivaluedMap formData) { - logger.debug("processLogin start"); - RealmManager realmManager = new RealmManager(session); - RealmModel realm = getAdminstrationRealm(realmManager); - if (realm == null) - throw new NotFoundException(); - ApplicationModel adminConsole = realm.getApplicationNameMap().get(Constants.ADMIN_CONSOLE_APPLICATION); - UserModel adminConsoleUser = adminConsole.getApplicationUser(); - - if (!realm.isEnabled()) { - throw new NotImplementedYetException(); - } - String username = formData.getFirst("username"); - UserModel user = realm.getUser(username); - - AuthenticationStatus status = authManager.authenticateForm(realm, user, formData); - - OAuthFlows oauth = Flows.oauth(realm, request, uriInfo, authManager, tokenManager); - - switch (status) { - case SUCCESS: - NewCookie cookie = authManager.createSaasIdentityCookie(realm, user, uriInfo); - return Response.status(302).cookie(cookie).location(contextRoot(uriInfo).path(adminPath).build()).build(); - case ACCOUNT_DISABLED: - return Flows.forms(realm, request, uriInfo).setError(Messages.ACCOUNT_DISABLED).setFormData(formData).createLogin(); - case ACTIONS_REQUIRED: - return oauth.processAccessCode(null, "n", contextRoot(uriInfo).path(adminPath).build().toString(), adminConsoleUser, user); - default: - return Flows.forms(realm, request, uriInfo).setError(Messages.INVALID_USER).setFormData(formData).createLogin(); - } + expireCookie(); } protected RealmModel getAdminstrationRealm(RealmManager realmManager) { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 2b5e420e75..5ea09877a8 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -2,10 +2,11 @@ package org.keycloak.services.resources.admin; import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.logging.Logger; +import org.keycloak.models.AdminRoles; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; @@ -20,7 +21,7 @@ import javax.ws.rs.core.Context; */ public class RealmAdminResource extends RoleContainerResource { protected static final Logger logger = Logger.getLogger(RealmAdminResource.class); - protected UserModel admin; + protected Auth auth; protected RealmModel realm; private TokenManager tokenManager; @@ -30,15 +31,17 @@ public class RealmAdminResource extends RoleContainerResource { @Context protected KeycloakSession session; - public RealmAdminResource(UserModel admin, RealmModel realm, TokenManager tokenManager) { + public RealmAdminResource(Auth auth, RealmModel realm, TokenManager tokenManager) { super(realm, realm); - this.admin = admin; + this.auth = auth; this.realm = realm; this.tokenManager = tokenManager; } @Path("applications") public ApplicationsResource getApplications() { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_APPLICATIONS); + ApplicationsResource applicationsResource = new ApplicationsResource(realm); resourceContext.initResource(applicationsResource); return applicationsResource; @@ -46,6 +49,8 @@ public class RealmAdminResource extends RoleContainerResource { @Path("oauth-clients") public OAuthClientsResource getOAuthClients() { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_CLIENTS); + OAuthClientsResource oauth = new OAuthClientsResource(realm, session); resourceContext.initResource(oauth); return oauth; @@ -55,26 +60,42 @@ public class RealmAdminResource extends RoleContainerResource { @NoCache @Produces("application/json") public RealmRepresentation getRealm() { - return ModelToRepresentation.toRepresentation(realm); - } + String realmAdminApp = AdminRoles.getAdminApp(realm); + if (auth.has(realmAdminApp, AdminRoles.MANAGE_REALM)) { + return ModelToRepresentation.toRepresentation(realm); + } else { + auth.requireOneOf(AdminRoles.getAdminApp(realm), AdminRoles.ALL_REALM_ROLES); + RealmRepresentation rep = new RealmRepresentation(); + rep.setId(realm.getId()); + rep.setRealm(realm.getName()); + + return rep; + } + } @PUT @Consumes("application/json") public void updateRealm(final RealmRepresentation rep) { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_REALM); + logger.debug("updating realm: " + realm.getName()); new RealmManager(session).updateRealm(rep, realm); } @DELETE public void deleteRealms() { - if (!session.removeRealm(realm.getId())) { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_REALM); + + if (!new RealmManager(session).removeRealm(realm)) { throw new NotFoundException(); } } @Path("users") public UsersResource users() { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_USERS); + UsersResource users = new UsersResource(realm, tokenManager); resourceContext.initResource(users); return users; @@ -82,12 +103,11 @@ public class RealmAdminResource extends RoleContainerResource { @Path("roles-by-id") public RoleByIdResource rolesById() { + auth.require(AdminRoles.getAdminApp(realm), AdminRoles.MANAGE_REALM); + RoleByIdResource resource = new RoleByIdResource(realm); resourceContext.initResource(resource); return resource; - } - - } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java index 48e91f8946..f0691d4517 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmsAdminResource.java @@ -5,10 +5,13 @@ import org.jboss.resteasy.logging.Logger; import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.jboss.resteasy.util.GenericType; +import org.keycloak.models.AdminRoles; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.services.managers.Auth; import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.managers.TokenManager; @@ -35,11 +38,11 @@ import java.util.Map; */ public class RealmsAdminResource { protected static final Logger logger = Logger.getLogger(RealmsAdminResource.class); - protected UserModel admin; + protected Auth auth; protected TokenManager tokenManager; - public RealmsAdminResource(UserModel admin, TokenManager tokenManager) { - this.admin = admin; + public RealmsAdminResource(Auth auth, TokenManager tokenManager) { + this.auth = auth; this.tokenManager = tokenManager; } @@ -59,11 +62,19 @@ public class RealmsAdminResource { @Produces("application/json") public List getRealms() { logger.debug(("getRealms()")); - RealmManager realmManager = new RealmManager(session); - List realms = session.getRealms(admin); + List realms = session.getRealms(); List reps = new ArrayList(); for (RealmModel realm : realms) { - reps.add(ModelToRepresentation.toRepresentation(realm)); + String realmAdminApp = AdminRoles.getAdminApp(realm); + + if (auth.has(realmAdminApp, AdminRoles.MANAGE_REALM)) { + reps.add(ModelToRepresentation.toRepresentation(realm)); + } else if (auth.hasOneOf(realmAdminApp, AdminRoles.ALL_REALM_ROLES)) { + RealmRepresentation rep = new RealmRepresentation(); + rep.setId(realm.getId()); + rep.setRealm(realm.getName()); + reps.add(rep); + } } return reps; } @@ -79,13 +90,15 @@ public class RealmsAdminResource { @POST @Consumes("application/json") public Response importRealm(@Context final UriInfo uriInfo, final RealmRepresentation rep) { + auth.require(AdminRoles.ADMIN); + logger.debug("importRealm: {0}", rep.getRealm()); RealmManager realmManager = new RealmManager(session); if (realmManager.getRealmByName(rep.getRealm()) != null) { return Flows.errors().exists("Realm " + rep.getRealm() + " already exists"); } - RealmModel realm = realmManager.importRealm(rep, admin); + RealmModel realm = realmManager.importRealm(rep); URI location = realmUrl(uriInfo).build(realm.getName()); logger.debug("imported realm success, sending back: {0}", location.toString()); return Response.created(location).build(); @@ -94,6 +107,8 @@ public class RealmsAdminResource { @POST @Consumes(MediaType.MULTIPART_FORM_DATA) public Response uploadRealm(MultipartFormDataInput input) throws IOException { + auth.require(AdminRoles.ADMIN); + Map> uploadForm = input.getFormDataMap(); List inputParts = uploadForm.get("file"); @@ -101,7 +116,7 @@ public class RealmsAdminResource { for (InputPart inputPart : inputParts) { inputPart.setMediaType(MediaType.APPLICATION_JSON_TYPE); RealmRepresentation rep = inputPart.getBody(new GenericType(){}); - realmManager.importRealm(rep, admin); + realmManager.importRealm(rep); } return Response.noContent().build(); } @@ -113,8 +128,11 @@ public class RealmsAdminResource { RealmModel realm = realmManager.getRealmByName(name); if (realm == null) throw new NotFoundException("{realm} = " + name); - RealmAdminResource adminResource = new RealmAdminResource(admin, realm, tokenManager); + auth.requireOneOf(AdminRoles.getAdminApp(realm), AdminRoles.ALL_REALM_ROLES); + + RealmAdminResource adminResource = new RealmAdminResource(auth, realm, tokenManager); resourceContext.initResource(adminResource); return adminResource; } + } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 24cfdc6eeb..2af61a9e8d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -23,7 +23,6 @@ import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; -import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.POST; import javax.ws.rs.PUT; @@ -31,12 +30,10 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; -import javax.ws.rs.ServerErrorException; import javax.ws.rs.container.ResourceContext; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; -import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -431,7 +428,7 @@ public class UsersResource { } String redirect = Urls.accountBase(uriInfo.getBaseUri()).path("/").build(realm.getName()).toString(); - String clientId = Constants.ACCOUNT_APPLICATION; + String clientId = Constants.ACCOUNT_MANAGEMENT_APP; String state = null; String scope = null; diff --git a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java index 6fbb6fde19..cfcd1f341e 100755 --- a/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java +++ b/testsuite/integration/src/main/java/org/keycloak/testutils/KeycloakServer.java @@ -228,7 +228,7 @@ public class KeycloakServer { try { RealmManager manager = new RealmManager(session); - RealmModel adminRealm = manager.getRealm(Constants.ADMIN_REALM); + RealmModel adminRealm = manager.getKeycloakAdminstrationRealm(); UserModel admin = adminRealm.getUser("admin"); admin.removeRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index a213b72ac6..8d9b742320 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -148,20 +148,6 @@ public class OAuthClient { } } - public UserRepresentation getProfile(String token) { - HttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(baseUrl + "/realms/" + realm + "/account"); - get.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token); - get.setHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType()); - - try { - HttpResponse response = client.execute(get); - return JsonSerialization.readValue(response.getEntity().getContent(), UserRepresentation.class); - } catch (Exception e) { - throw new RuntimeException("Failed to retrieve profile", e); - } - } - public AccessToken verifyToken(String token) { try { return RSATokenVerifier.verifyToken(token, realmPublicKey, realm); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java index f520c70ad9..3c96873be4 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/ProfileTest.java @@ -10,6 +10,7 @@ import org.json.JSONObject; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.models.AccountRoles; import org.keycloak.models.ApplicationModel; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; @@ -31,8 +32,6 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; import java.io.IOException; import java.net.URI; -import java.util.Collections; -import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -53,7 +52,7 @@ public class ProfileTest { user.setAttribute("key1", "value1"); user.setAttribute("key2", "value2"); - ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_APPLICATION); + ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP); UserModel user2 = appRealm.addUser("test-user-no-access@localhost"); user2.setEnabled(true); @@ -66,12 +65,12 @@ public class ProfileTest { appRealm.updateCredential(user2, creds); ApplicationModel app = appRealm.getApplicationNameMap().get("test-app"); - appRealm.addScopeMapping(app.getApplicationUser(), accountApp.getRole(org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE)); + appRealm.addScopeMapping(app.getApplicationUser(), accountApp.getRole(AccountRoles.VIEW_PROFILE)); app.getApplicationUser().addWebOrigin("http://localtest.me:8081"); UserModel thirdParty = appRealm.getUser("third-party"); - appRealm.addScopeMapping(thirdParty, accountApp.getRole(org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE)); + appRealm.addScopeMapping(thirdParty, accountApp.getRole(AccountRoles.VIEW_PROFILE)); } }); @@ -175,7 +174,7 @@ public class ProfileTest { @Test public void getProfileOAuthClient() throws Exception { - oauth.addScope(org.keycloak.models.Constants.ACCOUNT_APPLICATION, org.keycloak.models.Constants.ACCOUNT_PROFILE_ROLE); + oauth.addScope(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP, AccountRoles.VIEW_PROFILE); oauth.clientId("third-party"); oauth.doLoginGrant("test-user@localhost", "password"); @@ -192,7 +191,7 @@ public class ProfileTest { @Test public void getProfileOAuthClientNoScope() throws Exception { - oauth.addScope(org.keycloak.models.Constants.ACCOUNT_APPLICATION); + oauth.addScope(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP); oauth.clientId("third-party"); oauth.doLoginGrant("test-user@localhost", "password"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeImportRoleTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeImportRoleTest.java index 59a4c5ecb8..7a53ae09ea 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeImportRoleTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeImportRoleTest.java @@ -90,7 +90,7 @@ public class CompositeImportRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("APP_COMPOSITE_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "APP_COMPOSITE_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); @@ -115,7 +115,7 @@ public class CompositeImportRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_APP_COMPOSITE_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size()); Assert.assertTrue(token.getResourceAccess("APP_ROLE_APPLICATION").isUserInRole("APP_ROLE_1")); @@ -139,7 +139,7 @@ public class CompositeImportRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_COMPOSITE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject()); Assert.assertEquals(2, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_COMPOSITE_1")); @@ -162,7 +162,7 @@ public class CompositeImportRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_COMPOSITE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1")); @@ -184,7 +184,7 @@ public class CompositeImportRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_ROLE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_ROLE_1_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java index 648382305d..7d6c128c96 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/composites/CompositeRoleTest.java @@ -168,7 +168,7 @@ public class CompositeRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("APP_COMPOSITE_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "APP_COMPOSITE_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); @@ -193,7 +193,7 @@ public class CompositeRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_APP_COMPOSITE_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size()); Assert.assertTrue(token.getResourceAccess("APP_ROLE_APPLICATION").isUserInRole("APP_ROLE_1")); @@ -217,7 +217,7 @@ public class CompositeRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_COMPOSITE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject()); Assert.assertEquals(2, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_COMPOSITE_1")); @@ -240,7 +240,7 @@ public class CompositeRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_COMPOSITE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1")); @@ -262,7 +262,7 @@ public class CompositeRoleTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - Assert.assertEquals("REALM_ROLE_1_USER", oauth.getProfile(response.getAccessToken()).getUsername()); + Assert.assertEquals(keycloakRule.getUser("Test", "REALM_ROLE_1_USER").getId(), token.getSubject()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java index 4168e9f62d..0aa9075a33 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/AccountTest.java @@ -21,7 +21,6 @@ */ package org.keycloak.testsuite.forms; -import org.apache.http.HttpResponse; import org.junit.*; import org.keycloak.models.*; import org.keycloak.models.utils.TimeBasedOTP; @@ -35,10 +34,6 @@ import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.FindBy; - -import java.util.List; import static org.junit.Assert.assertEquals; @@ -53,7 +48,7 @@ public class AccountTest { public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { UserModel user = appRealm.getUser("test-user@localhost"); - ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_APPLICATION); + ApplicationModel accountApp = appRealm.getApplicationNameMap().get(org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_APP); UserModel user2 = appRealm.addUser("test-user-no-access@localhost"); user2.setEnabled(true); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 964a7d3a56..b894e2c890 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -25,8 +25,10 @@ import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.OAuthClient.AccessTokenResponse; import org.keycloak.testsuite.pages.LoginPage; @@ -70,11 +72,8 @@ public class AccessTokenTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); - UserRepresentation user = oauth.getProfile(response.getAccessToken()); - - Assert.assertEquals(user.getId(), token.getSubject()); + Assert.assertEquals(keycloakRule.getUser("test", "test-user@localhost").getId(), token.getSubject()); Assert.assertNotEquals("test-user@localhost", token.getSubject()); - Assert.assertEquals("test-user@localhost", user.getUsername()); Assert.assertEquals(1, token.getRealmAccess().getRoles().size()); Assert.assertTrue(token.getRealmAccess().isUserInRole("user")); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java index caf6c58172..a595694dd0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java @@ -7,6 +7,8 @@ import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.services.managers.ModelToRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testutils.KeycloakServer; import org.keycloak.util.JsonSerialization; @@ -31,6 +33,24 @@ public abstract class AbstractKeycloakRule extends ExternalResource { setupKeycloak(); } + public UserRepresentation getUser(String realm, String name) { + KeycloakSession session = server.getKeycloakSessionFactory().createSession(); + try { + return ModelToRepresentation.toRepresentation(session.getRealmByName(realm).getUser(name)); + } finally { + session.close(); + } + } + + public UserRepresentation getUserById(String realm, String id) { + KeycloakSession session = server.getKeycloakSessionFactory().createSession(); + try { + return ModelToRepresentation.toRepresentation(session.getRealmByName(realm).getUserById(id)); + } finally { + session.close(); + } + } + protected void setupKeycloak() { KeycloakSession session = server.getKeycloakSessionFactory().createSession(); session.getTransaction().begin(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java index 7f4e17ad57..27e7a871c6 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/social/SocialLoginTest.java @@ -26,6 +26,9 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; +import org.keycloak.models.AccountRoles; +import org.keycloak.models.ApplicationModel; +import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.UserRepresentation; @@ -107,7 +110,7 @@ public class SocialLoginTest { AccessToken token = oauth.verifyToken(response.getAccessToken()); Assert.assertEquals(36, token.getSubject().length()); - UserRepresentation profile = oauth.getProfile(response.getAccessToken()); + UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject()); Assert.assertEquals(36, profile.getUsername().length()); Assert.assertEquals("Bob", profile.getFirstName()); @@ -146,7 +149,9 @@ public class SocialLoginTest { Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); AccessTokenResponse response = oauth.doAccessTokenRequest(oauth.getCurrentQuery().get("code"), "password"); - UserRepresentation profile = oauth.getProfile(response.getAccessToken()); + AccessToken token = oauth.verifyToken(response.getAccessToken()); + + UserRepresentation profile = keycloakRule.getUserById("test", token.getSubject()); Assert.assertEquals("Dummy", profile.getFirstName()); Assert.assertEquals("User", profile.getLastName());