| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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.apache.nifi.registry.web.api; |
| |
| import io.jsonwebtoken.JwtException; |
| import io.swagger.annotations.Api; |
| import io.swagger.annotations.ApiOperation; |
| import io.swagger.annotations.ApiResponse; |
| import io.swagger.annotations.ApiResponses; |
| import io.swagger.annotations.Authorization; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.nifi.registry.authorization.CurrentUser; |
| import org.apache.nifi.registry.event.EventService; |
| import org.apache.nifi.registry.exception.AdministrationException; |
| import org.apache.nifi.registry.properties.NiFiRegistryProperties; |
| import org.apache.nifi.registry.security.authentication.AuthenticationRequest; |
| import org.apache.nifi.registry.security.authentication.AuthenticationResponse; |
| import org.apache.nifi.registry.security.authentication.BasicAuthIdentityProvider; |
| import org.apache.nifi.registry.security.authentication.IdentityProvider; |
| import org.apache.nifi.registry.security.authentication.IdentityProviderUsage; |
| import org.apache.nifi.registry.security.authentication.exception.IdentityAccessException; |
| import org.apache.nifi.registry.security.authentication.exception.InvalidCredentialsException; |
| import org.apache.nifi.registry.security.authorization.user.NiFiUser; |
| import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils; |
| import org.apache.nifi.registry.service.AuthorizationService; |
| import org.apache.nifi.registry.web.exception.UnauthorizedException; |
| import org.apache.nifi.registry.web.security.authentication.jwt.JwtService; |
| import org.apache.nifi.registry.web.security.authentication.kerberos.KerberosSpnegoIdentityProvider; |
| import org.apache.nifi.registry.web.security.authentication.x509.X509IdentityProvider; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.lang.Nullable; |
| import org.springframework.stereotype.Component; |
| |
| import javax.servlet.http.HttpServletRequest; |
| import javax.ws.rs.Consumes; |
| import javax.ws.rs.GET; |
| import javax.ws.rs.POST; |
| import javax.ws.rs.Path; |
| import javax.ws.rs.Produces; |
| import javax.ws.rs.WebApplicationException; |
| import javax.ws.rs.core.Context; |
| import javax.ws.rs.core.MediaType; |
| import javax.ws.rs.core.Response; |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.stream.Collectors; |
| |
| @Component |
| @Path("/access") |
| @Api( |
| value = "access", |
| description = "Endpoints for obtaining an access token or checking access status." |
| ) |
| public class AccessResource extends ApplicationResource { |
| |
| private static final Logger logger = LoggerFactory.getLogger(AccessResource.class); |
| |
| private NiFiRegistryProperties properties; |
| private AuthorizationService authorizationService; |
| private JwtService jwtService; |
| private X509IdentityProvider x509IdentityProvider; |
| private KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider; |
| private IdentityProvider identityProvider; |
| |
| @Autowired |
| public AccessResource( |
| NiFiRegistryProperties properties, |
| AuthorizationService authorizationService, |
| JwtService jwtService, |
| X509IdentityProvider x509IdentityProvider, |
| @Nullable KerberosSpnegoIdentityProvider kerberosSpnegoIdentityProvider, |
| @Nullable IdentityProvider identityProvider, |
| EventService eventService) { |
| super(eventService); |
| this.properties = properties; |
| this.jwtService = jwtService; |
| this.x509IdentityProvider = x509IdentityProvider; |
| this.kerberosSpnegoIdentityProvider = kerberosSpnegoIdentityProvider; |
| this.identityProvider = identityProvider; |
| this.authorizationService = authorizationService; |
| } |
| |
| /** |
| * Gets the current client's identity and authorized permissions. |
| * |
| * @param httpServletRequest the servlet request |
| * @return An object describing the current client identity, as determined by the server, and it's permissions. |
| */ |
| @GET |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.APPLICATION_JSON) |
| @ApiOperation( |
| value = "Returns the current client's authenticated identity and permissions to top-level resources", |
| response = CurrentUser.class, |
| authorizations = {@Authorization(value = "Authorization")} |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might be running unsecured.") }) |
| public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) { |
| |
| final NiFiUser user = NiFiUserUtils.getNiFiUser(); |
| if (user == null) { |
| // Not expected to happen unless the nifi registry server has been seriously misconfigured. |
| throw new WebApplicationException(new Throwable("Unable to access details for current user.")); |
| } |
| |
| final CurrentUser currentUser = authorizationService.getCurrentUser(); |
| |
| return generateOkResponse(currentUser).build(); |
| } |
| |
| |
| /** |
| * Creates a token for accessing the REST API. |
| * |
| * @param httpServletRequest the servlet request |
| * @return A JWT (string) |
| */ |
| @POST |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token") |
| @ApiOperation( |
| value = "Creates a token for accessing the REST API via auto-detected method of verifying client identity claim credentials", |
| notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + |
| "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + |
| "in the format 'Authorization: Bearer <token>'.", |
| response = String.class |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response createAccessTokenByTryingAllProviders(@Context HttpServletRequest httpServletRequest) { |
| |
| // only support access tokens when communicating over HTTPS |
| if (!httpServletRequest.isSecure()) { |
| throw new IllegalStateException("Access tokens are only issued over HTTPS"); |
| } |
| |
| List<IdentityProvider> identityProviderWaterfall = generateIdentityProviderWaterfall(); |
| |
| String token = null; |
| for (IdentityProvider provider : identityProviderWaterfall) { |
| |
| AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); |
| if (authenticationRequest == null) { |
| continue; |
| } |
| try { |
| token = createAccessToken(identityProvider, authenticationRequest); |
| break; |
| } catch (final InvalidCredentialsException ice){ |
| logger.debug("{}: the supplied client credentials are invalid.", identityProvider.getClass().getSimpleName()); |
| logger.debug("", ice); |
| } |
| |
| } |
| |
| if (StringUtils.isEmpty(token)) { |
| List<IdentityProviderUsage.AuthType> acceptableAuthTypes = identityProviderWaterfall.stream() |
| .map(IdentityProvider::getUsageInstructions) |
| .map(IdentityProviderUsage::getAuthType) |
| .filter(Objects::nonNull) |
| .distinct() |
| .collect(Collectors.toList()); |
| |
| throw new UnauthorizedException("Client credentials are missing or invalid according to all configured identity providers.") |
| .withAuthenticateChallenge(acceptableAuthTypes); |
| } |
| |
| // build the response |
| final URI uri = URI.create(generateResourceUri("access", "token")); |
| return generateCreatedResponse(uri, token).build(); |
| } |
| |
| /** |
| * Creates a token for accessing the REST API. |
| * |
| * @param httpServletRequest the servlet request |
| * @return A JWT (string) |
| */ |
| @POST |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token/login") |
| @ApiOperation( |
| value = "Creates a token for accessing the REST API via username/password", |
| notes = "The user credentials must be passed in standard HTTP Basic Auth format. " + |
| "That is: 'Authorization: Basic <credentials>', where <credentials> is the base64 encoded value of '<username>:<password>'. " + |
| "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + |
| "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + |
| "in the format 'Authorization: Bearer <token>'.", |
| response = String.class, |
| authorizations = { @Authorization("BasicAuth") } |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with username/password."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response createAccessTokenUsingBasicAuthCredentials(@Context HttpServletRequest httpServletRequest) { |
| |
| // only support access tokens when communicating over HTTPS |
| if (!httpServletRequest.isSecure()) { |
| throw new IllegalStateException("Access tokens are only issued over HTTPS"); |
| } |
| |
| // if not configured with custom identity provider, or if provider doesn't support HTTP Basic Auth, don't consider credentials |
| if (identityProvider == null) { |
| logger.debug("An Identity Provider must be configured to use this endpoint. Please consult the administration guide."); |
| throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator."); |
| } |
| if (!(identityProvider instanceof BasicAuthIdentityProvider)) { |
| logger.debug("An Identity Provider is configured, but it does not support HTTP Basic Auth authentication. " + |
| "The configured Identity Provider must extend {}", BasicAuthIdentityProvider.class); |
| throw new IllegalStateException("Username/Password login not supported by this NiFi. Contact System Administrator."); |
| } |
| |
| // generate JWT for response |
| AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); |
| |
| if (authenticationRequest == null) { |
| throw new UnauthorizedException("The client credentials are missing from the request.") |
| .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER); |
| } |
| |
| final String token; |
| try { |
| token = createAccessToken(identityProvider, authenticationRequest); |
| } catch (final InvalidCredentialsException ice){ |
| throw new UnauthorizedException("The supplied client credentials are not valid.", ice) |
| .withAuthenticateChallenge(IdentityProviderUsage.AuthType.OTHER); |
| } |
| |
| // form the response |
| final URI uri = URI.create(generateResourceUri("access", "token")); |
| return generateCreatedResponse(uri, token).build(); |
| } |
| |
| @POST |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token/kerberos") |
| @ApiOperation( |
| value = "Creates a token for accessing the REST API via Kerberos Service Tickets or SPNEGO Tokens (which includes Kerberos Service Tickets)", |
| notes = "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + |
| "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + |
| "in the format 'Authorization: Bearer <token>'.", |
| response = String.class |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login Kerberos credentials."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response createAccessTokenUsingKerberosTicket(@Context HttpServletRequest httpServletRequest) { |
| |
| // only support access tokens when communicating over HTTPS |
| if (!httpServletRequest.isSecure()) { |
| throw new IllegalStateException("Access tokens are only issued over HTTPS"); |
| } |
| |
| // if not configured with custom identity provider, don't consider credentials |
| if (!properties.isKerberosSpnegoSupportEnabled() || kerberosSpnegoIdentityProvider == null) { |
| throw new IllegalStateException("Kerberos service ticket login not supported by this NiFi Registry"); |
| } |
| |
| AuthenticationRequest authenticationRequest = kerberosSpnegoIdentityProvider.extractCredentials(httpServletRequest); |
| |
| if (authenticationRequest == null) { |
| throw new UnauthorizedException("The client credentials are missing from the request.") |
| .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType()); |
| } |
| |
| final String token; |
| try { |
| token = createAccessToken(kerberosSpnegoIdentityProvider, authenticationRequest); |
| } catch (final InvalidCredentialsException ice){ |
| throw new UnauthorizedException("The supplied client credentials are not valid.", ice) |
| .withAuthenticateChallenge(kerberosSpnegoIdentityProvider.getUsageInstructions().getAuthType()); |
| } |
| |
| // build the response |
| final URI uri = URI.create(generateResourceUri("access", "token")); |
| return generateCreatedResponse(uri, token).build(); |
| |
| } |
| |
| /** |
| * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. |
| * |
| * @param httpServletRequest the servlet request |
| * @return A JWT (string) |
| */ |
| @POST |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token/identity-provider") |
| @ApiOperation( |
| value = "Creates a token for accessing the REST API via a custom identity provider.", |
| notes = "The user credentials must be passed in a format understood by the custom identity provider, e.g., a third-party auth token in an HTTP header. " + |
| "The exact format of the user credentials expected by the custom identity provider can be discovered by 'GET /access/token/identity-provider/usage'. " + |
| "The token returned is formatted as a JSON Web Token (JWT). The token is base64 encoded and comprised of three parts. The header, " + |
| "the body, and the signature. The expiration of the token is a contained within the body. The token can be used in the Authorization header " + |
| "in the format 'Authorization: Bearer <token>'.", |
| response = String.class |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response createAccessTokenUsingIdentityProviderCredentials(@Context HttpServletRequest httpServletRequest) { |
| |
| // only support access tokens when communicating over HTTPS |
| if (!httpServletRequest.isSecure()) { |
| throw new IllegalStateException("Access tokens are only issued over HTTPS"); |
| } |
| |
| // if not configured with custom identity provider, don't consider credentials |
| if (identityProvider == null) { |
| throw new IllegalStateException("Custom login not supported by this NiFi Registry"); |
| } |
| |
| AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); |
| |
| if (authenticationRequest == null) { |
| throw new UnauthorizedException("The client credentials are missing from the request.") |
| .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); |
| } |
| |
| final String token; |
| try { |
| token = createAccessToken(identityProvider, authenticationRequest); |
| } catch (InvalidCredentialsException ice) { |
| throw new UnauthorizedException("The supplied client credentials are not valid.", ice) |
| .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); |
| } |
| |
| // build the response |
| final URI uri = URI.create(generateResourceUri("access", "token")); |
| return generateCreatedResponse(uri, token).build(); |
| |
| } |
| |
| /** |
| * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. |
| * |
| * @param httpServletRequest the servlet request |
| * @return A JWT (string) |
| */ |
| @GET |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token/identity-provider/usage") |
| @ApiOperation( |
| value = "Provides a description of how the currently configured identity provider expects credentials to be passed to POST /access/token/identity-provider", |
| response = String.class |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response getIdentityProviderUsageInstructions(@Context HttpServletRequest httpServletRequest) { |
| |
| // if not configuration for login, don't consider credentials |
| if (identityProvider == null) { |
| throw new IllegalStateException("Custom login not supported by this NiFi Registry"); |
| } |
| |
| Class ipClazz = identityProvider.getClass(); |
| String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); |
| |
| try { |
| String usageInstructions = "Usage Instructions for '" + identityProviderName + "': "; |
| usageInstructions += identityProvider.getUsageInstructions().getText(); |
| return generateOkResponse(usageInstructions).build(); |
| |
| } catch (Exception e) { |
| // If, for any reason, this identity provider does not support getUsageInstructions(), e.g., returns null or throws NotImplementedException. |
| return Response.status(Response.Status.NOT_IMPLEMENTED) |
| .entity("The currently configured identity provider, '" + identityProvider.getClass().getName() + "' does not provide usage instructions.") |
| .build(); |
| } |
| |
| } |
| |
| /** |
| * Creates a token for accessing the REST API using a custom identity provider configured using NiFi Registry extensions. |
| * |
| * @param httpServletRequest the servlet request |
| * @return A JWT (string) |
| */ |
| @POST |
| @Consumes(MediaType.WILDCARD) |
| @Produces(MediaType.TEXT_PLAIN) |
| @Path("/token/identity-provider/test") |
| @ApiOperation( |
| value = "Tests the format of the credentials against this identity provider without preforming authentication on the credentials to validate them.", |
| notes = "The user credentials should be passed in a format understood by the custom identity provider as defined by 'GET /access/token/identity-provider/usage'.", |
| response = String.class |
| ) |
| @ApiResponses({ |
| @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), |
| @ApiResponse(code = 401, message = "The format of the credentials were not recognized by the currently configured identity provider."), |
| @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry may not be configured to support login with customized credentials."), |
| @ApiResponse(code = 500, message = HttpStatusMessages.MESSAGE_500) }) |
| public Response testIdentityProviderRecognizesCredentialsFormat(@Context HttpServletRequest httpServletRequest) { |
| |
| // only support access tokens when communicating over HTTPS |
| if (!httpServletRequest.isSecure()) { |
| throw new IllegalStateException("Access tokens are only issued over HTTPS"); |
| } |
| |
| // if not configured with custom identity provider, don't consider credentials |
| if (identityProvider == null) { |
| throw new IllegalStateException("Custom login not supported by this NiFi Registry"); |
| } |
| |
| final Class ipClazz = identityProvider.getClass(); |
| final String identityProviderName = StringUtils.isNotEmpty(ipClazz.getSimpleName()) ? ipClazz.getSimpleName() : ipClazz.getName(); |
| |
| // attempt to extract client credentials without authenticating them |
| AuthenticationRequest authenticationRequest = identityProvider.extractCredentials(httpServletRequest); |
| |
| if (authenticationRequest == null) { |
| throw new UnauthorizedException("The format of the credentials were not recognized by the currently configured identity provider " + |
| "'" + identityProviderName + "'. " + identityProvider.getUsageInstructions().getText()) |
| .withAuthenticateChallenge(identityProvider.getUsageInstructions().getAuthType()); |
| } |
| |
| |
| final String successMessage = identityProviderName + " recognized the format of the credentials in the HTTP request."; |
| return generateOkResponse(successMessage).build(); |
| |
| } |
| |
| private String createAccessToken(IdentityProvider identityProvider, AuthenticationRequest authenticationRequest) |
| throws InvalidCredentialsException, AdministrationException { |
| |
| final AuthenticationResponse authenticationResponse; |
| |
| try { |
| authenticationResponse = identityProvider.authenticate(authenticationRequest); |
| final String token = jwtService.generateSignedToken(authenticationResponse); |
| return token; |
| } catch (final IdentityAccessException | JwtException e) { |
| throw new AdministrationException(e.getMessage()); |
| } |
| |
| } |
| |
| /** |
| * A helper function that generates a prioritized list of IdentityProviders to use to |
| * attempt client authentication. |
| * |
| * Note: This is currently a hard-coded list order consisting of: |
| * |
| * - X509IdentityProvider (if available) |
| * - KerberosProvider (if available) |
| * - User-defined IdentityProvider (if available) |
| * |
| * However, in the future it could be entirely user-configurable |
| * |
| * @return a list of providers to use in order to authenticate the client. |
| */ |
| private List<IdentityProvider> generateIdentityProviderWaterfall() { |
| List<IdentityProvider> identityProviderWaterfall = new ArrayList<>(); |
| |
| // if configured with an X509IdentityProvider, add it to the list of providers to try |
| if (x509IdentityProvider != null) { |
| identityProviderWaterfall.add(x509IdentityProvider); |
| } |
| |
| // if configured with an KerberosSpnegoIdentityProvider, add it to the end of the list of providers to try |
| if (kerberosSpnegoIdentityProvider != null) { |
| identityProviderWaterfall.add(kerberosSpnegoIdentityProvider); |
| } |
| |
| // if configured with custom identity provider, add it to the end of the list of providers to try |
| if (identityProvider != null) { |
| identityProviderWaterfall.add(identityProvider); |
| } |
| |
| return identityProviderWaterfall; |
| } |
| |
| } |