/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.keycloak.services.resources.admin; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.jboss.resteasy.spi.NoLogWebApplicationException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.common.ClientConnection; import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInputException; import org.keycloak.models.AdminRoles; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.AccessToken; import org.keycloak.services.ForbiddenException; import org.keycloak.services.managers.AppAuthManager; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.admin.info.ServerInfoAdminResource; import org.keycloak.theme.Theme; import org.keycloak.theme.ThemeProvider; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.util.Locale; import java.util.Properties; /** * Root resource for admin console and admin REST API * * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 1 $ */ @Path("/admin") public class AdminRoot { protected static final Logger logger = Logger.getLogger(AdminRoot.class); @Context protected UriInfo uriInfo; @Context protected ClientConnection clientConnection; @Context protected HttpRequest request; @Context protected HttpResponse response; protected AppAuthManager authManager; protected TokenManager tokenManager; @Context protected KeycloakSession session; public AdminRoot() { this.tokenManager = new TokenManager(); this.authManager = new AppAuthManager(); } public static UriBuilder adminBaseUrl(UriInfo uriInfo) { return adminBaseUrl(uriInfo.getBaseUriBuilder()); } public static UriBuilder adminBaseUrl(UriBuilder base) { return base.path(AdminRoot.class); } /** * Convenience path to master realm admin console * * @exclude * @return */ @GET public Response masterRealmAdminConsoleRedirect() { RealmModel master = new RealmManager(session).getKeycloakAdminstrationRealm(); return Response.status(302).location( uriInfo.getBaseUriBuilder().path(AdminRoot.class).path(AdminRoot.class, "getAdminConsole").path("/").build(master.getName()) ).build(); } /** * Convenience path to master realm admin console * * @exclude * @return */ @Path("index.{html:html}") // expression is actually "index.html" but this is a hack to get around jax-doclet bug @GET public Response masterRealmAdminConsoleRedirectHtml() { return masterRealmAdminConsoleRedirect(); } protected RealmModel locateRealm(String name, RealmManager realmManager) { RealmModel realm = realmManager.getRealmByName(name); if (realm == null) { throw new NotFoundException("Realm not found. Did you type in a bad URL?"); } session.getContext().setRealm(realm); return realm; } public static UriBuilder adminConsoleUrl(UriInfo uriInfo) { return adminConsoleUrl(uriInfo.getBaseUriBuilder()); } public static UriBuilder adminConsoleUrl(UriBuilder base) { return adminBaseUrl(base).path(AdminRoot.class, "getAdminConsole"); } /** * path to realm admin console ui * * @exclude * @param name Realm name (not id!) * @return */ @Path("{realm}/console") public AdminConsole getAdminConsole(final @PathParam("realm") String name) { RealmManager realmManager = new RealmManager(session); RealmModel realm = locateRealm(name, realmManager); AdminConsole service = new AdminConsole(realm); ResteasyProviderFactory.getInstance().injectProperties(service); return service; } protected AdminAuth authenticateRealmAdminRequest(HttpHeaders headers) { String tokenString = authManager.extractAuthorizationHeaderToken(headers); if (tokenString == null) throw new UnauthorizedException("Bearer"); AccessToken token; try { JWSInput input = new JWSInput(tokenString); token = input.readJsonContent(AccessToken.class); } catch (JWSInputException e) { throw new UnauthorizedException("Bearer token format error"); } String realmName = token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1); RealmManager realmManager = new RealmManager(session); RealmModel realm = realmManager.getRealmByName(realmName); if (realm == null) { throw new UnauthorizedException("Unknown realm in token"); } session.getContext().setRealm(realm); AuthenticationManager.AuthResult authResult = authManager.authenticateBearerToken(session, realm, uriInfo, clientConnection, headers); if (authResult == null) { logger.debug("Token not valid"); throw new UnauthorizedException("Bearer"); } ClientModel client = realm.getClientByClientId(token.getIssuedFor()); if (client == null) { throw new NotFoundException("Could not find client for authorization"); } return new AdminAuth(realm, authResult.getToken(), authResult.getUser(), client); } public static UriBuilder realmsUrl(UriInfo uriInfo) { return realmsUrl(uriInfo.getBaseUriBuilder()); } public static UriBuilder realmsUrl(UriBuilder base) { return adminBaseUrl(base).path(AdminRoot.class, "getRealmsAdmin"); } /** * Base Path to realm admin REST interface * * @param headers * @return */ @Path("realms") public RealmsAdminResource getRealmsAdmin(@Context final HttpHeaders headers) { handlePreflightRequest(); AdminAuth auth = authenticateRealmAdminRequest(headers); if (auth != null) { logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); } Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").exposedHeaders("Location").auth().build(response); RealmsAdminResource adminResource = new RealmsAdminResource(auth, tokenManager); ResteasyProviderFactory.getInstance().injectProperties(adminResource); return adminResource; } /** * General information about the server * * @param headers * @return */ @Path("serverinfo") public ServerInfoAdminResource getServerInfo(@Context final HttpHeaders headers) { handlePreflightRequest(); AdminAuth auth = authenticateRealmAdminRequest(headers); if (!isAdmin(auth)) { throw new ForbiddenException(); } if (auth != null) { logger.debug("authenticated admin access for: " + auth.getUser().getUsername()); } Cors.add(request).allowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(response); ServerInfoAdminResource adminResource = new ServerInfoAdminResource(); ResteasyProviderFactory.getInstance().injectProperties(adminResource); return adminResource; } protected boolean isAdmin(AdminAuth auth) { RealmManager realmManager = new RealmManager(session); if (auth.getRealm().equals(realmManager.getKeycloakAdminstrationRealm())) { if (auth.hasOneOfRealmRole(AdminRoles.ADMIN, AdminRoles.CREATE_REALM)) { return true; } for (RealmModel realm : session.realms().getRealms()) { ClientModel client = realm.getMasterAdminClient(); if (auth.hasOneOfAppRole(client, AdminRoles.ALL_REALM_ROLES)) { return true; } } return false; } else { ClientModel client = auth.getRealm().getClientByClientId(realmManager.getRealmAdminClientId(auth.getRealm())); return auth.hasOneOfAppRole(client, AdminRoles.ALL_REALM_ROLES); } } protected void handlePreflightRequest() { if (request.getHttpMethod().equalsIgnoreCase("OPTIONS")) { logger.debug("Cors admin pre-flight"); Response response = Cors.add(request, Response.ok()).preflight().allowedMethods("GET", "PUT", "POST", "DELETE").auth().build(); throw new NoLogWebApplicationException(response); } } public static Theme getTheme(KeycloakSession session, RealmModel realm) throws IOException { ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); return themeProvider.getTheme(realm.getAdminTheme(), Theme.Type.ADMIN); } public static Properties getMessages(KeycloakSession session, RealmModel realm, String lang) { try { Theme theme = getTheme(session, realm); Locale locale = lang != null ? Locale.forLanguageTag(lang) : Locale.ENGLISH; return theme.getMessages(locale); } catch (IOException e) { logger.error("Failed to load messages from theme", e); return new Properties(); } } public static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String... bundles) { Properties compound = new Properties(); for (String bundle : bundles) { Properties current = getMessages(session, realm, lang, bundle); compound.putAll(current); } return compound; } private static Properties getMessages(KeycloakSession session, RealmModel realm, String lang, String bundle) { try { Theme theme = getTheme(session, realm); Locale locale = lang != null ? Locale.forLanguageTag(lang) : Locale.ENGLISH; return theme.getMessages(bundle, locale); } catch (IOException e) { logger.error("Failed to load messages from theme", e); return new Properties(); } } }