/* * 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; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.UnauthorizedException; import org.keycloak.OAuthErrorException; import org.keycloak.common.ClientConnection; import org.keycloak.common.util.Time; import org.keycloak.constants.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil; import org.keycloak.representations.idm.OAuth2ErrorRepresentation; import org.keycloak.services.ForbiddenException; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; 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.Response; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class ClientsManagementService { private static final Logger logger = Logger.getLogger(ClientsManagementService.class); private RealmModel realm; private EventBuilder event; @Context private HttpRequest request; @Context protected HttpHeaders headers; @Context private UriInfo uriInfo; @Context private ClientConnection clientConnection; @Context protected Providers providers; @Context protected KeycloakSession session; public ClientsManagementService(RealmModel realm, EventBuilder event) { this.realm = realm; this.event = event; } public static UriBuilder clientsManagementBaseUrl(UriBuilder baseUriBuilder) { return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getClientsManagementService"); } public static UriBuilder registerNodeUrl(UriBuilder baseUriBuilder) { UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder); return uriBuilder.path(ClientsManagementService.class, "registerNode"); } public static UriBuilder unregisterNodeUrl(UriBuilder baseUriBuilder) { UriBuilder uriBuilder = clientsManagementBaseUrl(baseUriBuilder); return uriBuilder.path(ClientsManagementService.class, "unregisterNode"); } /** * URL invoked by adapter to register new client cluster node. Each application cluster node will invoke this URL once it joins cluster * * @param authorizationHeader * @param formData * @return */ @Path("register-node") @POST @Produces(MediaType.APPLICATION_JSON) public Response registerNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap<String, String> formData) { if (!checkSsl()) { throw new ForbiddenException("HTTPS required"); } event.event(EventType.REGISTER_NODE); if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); throw new UnauthorizedException("Realm not enabled"); } ClientModel client = authorizeClient(); String nodeHost = getClientClusterHost(formData); event.client(client).detail(Details.NODE_HOST, nodeHost); logger.debugf("Registering cluster host '%s' for client '%s'", nodeHost, client.getClientId()); client.registerNode(nodeHost, Time.currentTime()); event.success(); return Response.noContent().build(); } /** * URL invoked by adapter to register new client cluster node. Each application cluster node will invoke this URL once it joins cluster * * @param authorizationHeader * @param formData * @return */ @Path("unregister-node") @POST @Produces(MediaType.APPLICATION_JSON) public Response unregisterNode(@HeaderParam(HttpHeaders.AUTHORIZATION) String authorizationHeader, final MultivaluedMap<String, String> formData) { if (!checkSsl()) { throw new ForbiddenException("HTTPS required"); } event.event(EventType.UNREGISTER_NODE); if (!realm.isEnabled()) { event.error(Errors.REALM_DISABLED); throw new UnauthorizedException("Realm not enabled"); } ClientModel client = authorizeClient(); String nodeHost = getClientClusterHost(formData); event.client(client).detail(Details.NODE_HOST, nodeHost); logger.debugf("Unregistering cluster host '%s' for client '%s'", nodeHost, client.getClientId()); client.unregisterNode(nodeHost); event.success(); return Response.noContent().build(); } protected ClientModel authorizeClient() { ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient(); if (client.isPublicClient()) { OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(OAuthErrorException.INVALID_CLIENT, "Public clients not allowed"); event.error(Errors.INVALID_CLIENT); throw new BadRequestException("Public clients not allowed", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build()); } return client; } protected String getClientClusterHost(MultivaluedMap<String, String> formData) { String clientClusterHost = formData.getFirst(AdapterConstants.CLIENT_CLUSTER_HOST); if (clientClusterHost == null || clientClusterHost.length() == 0) { OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation( OAuthErrorException.INVALID_REQUEST, "Client cluster host not specified"); event.error(Errors.INVALID_CODE); throw new BadRequestException("Cluster host not specified", javax.ws.rs.core.Response.status(javax.ws.rs.core.Response.Status.BAD_REQUEST).entity(errorRep).type(MediaType.APPLICATION_JSON_TYPE).build()); } return clientClusterHost; } private boolean checkSsl() { if (uriInfo.getBaseUri().getScheme().equals("https")) { return true; } else { return !realm.getSslRequired().isRequired(clientConnection); } } }