http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java ---------------------------------------------------------------------- diff --git a/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java new file mode 100644 index 0000000..4950772 --- /dev/null +++ b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java @@ -0,0 +1,91 @@ +/* + * 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.util; + +import java.util.concurrent.TimeUnit; + +/** + * <p> + * A PropertyValue provides a mechanism whereby the currently configured value + * can be obtained in different forms. + * </p> + */ +public interface PropertyValue { + + /** + * @return the raw property value as a string + */ + String getValue(); + + /** + * @return an integer representation of the property value, or + * <code>null</code> if not set + * @throws NumberFormatException if not able to parse + */ + Integer asInteger(); + + /** + * @return a Long representation of the property value, or <code>null</code> + * if not set + * @throws NumberFormatException if not able to parse + */ + Long asLong(); + + /** + * @return a Boolean representation of the property value, or + * <code>null</code> if not set + */ + Boolean asBoolean(); + + /** + * @return a Float representation of the property value, or + * <code>null</code> if not set + * @throws NumberFormatException if not able to parse + */ + Float asFloat(); + + /** + * @return a Double representation of the property value, of + * <code>null</code> if not set + * @throws NumberFormatException if not able to parse + */ + Double asDouble(); + + /** + * @param timeUnit specifies the TimeUnit to convert the time duration into + * @return a Long value representing the value of the configured time period + * in terms of the specified TimeUnit; if the property is not set, returns + * <code>null</code> + */ + Long asTimePeriod(TimeUnit timeUnit); + + /** + * + * @param dataUnit specifies the DataUnit to convert the data size into + * @return a Long value representing the value of the configured data size + * in terms of the specified DataUnit; if hte property is not set, returns + * <code>null</code> + */ + Double asDataSize(DataUnit dataUnit); + + /** + * @return <code>true</code> if the user has configured a value, or if the + * PropertyDescriptor for the associated property has a default + * value, <code>false</code> otherwise + */ + boolean isSet(); +}
http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java ---------------------------------------------------------------------- diff --git a/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java new file mode 100644 index 0000000..b185fad --- /dev/null +++ b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java @@ -0,0 +1,79 @@ +/* + * 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.util; + +import java.util.concurrent.TimeUnit; + +public class StandardPropertyValue implements PropertyValue { + + private final String rawValue; + + public StandardPropertyValue(final String rawValue) { + this.rawValue = rawValue; + } + + @Override + public String getValue() { + return rawValue; + } + + @Override + public Integer asInteger() { + return (rawValue == null) ? null : Integer.parseInt(rawValue.trim()); + } + + @Override + public Long asLong() { + return (rawValue == null) ? null : Long.parseLong(rawValue.trim()); + } + + @Override + public Boolean asBoolean() { + return (rawValue == null) ? null : Boolean.parseBoolean(rawValue.trim()); + } + + @Override + public Float asFloat() { + return (rawValue == null) ? null : Float.parseFloat(rawValue.trim()); + } + + @Override + public Double asDouble() { + return (rawValue == null) ? null : Double.parseDouble(rawValue.trim()); + } + + @Override + public Long asTimePeriod(final TimeUnit timeUnit) { + return (rawValue == null) ? null : FormatUtils.getTimeDuration(rawValue.trim(), timeUnit); + } + + @Override + public Double asDataSize(final DataUnit dataUnit) { + return rawValue == null ? null : DataUnit.parseDataSize(rawValue.trim(), dataUnit); + } + + @Override + public boolean isSet() { + return rawValue != null; + } + + @Override + public String toString() { + return rawValue; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/pom.xml ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/pom.xml b/nifi-registry-web-api/pom.xml index 603cb68..4aa7f15 100644 --- a/nifi-registry-web-api/pom.xml +++ b/nifi-registry-web-api/pom.xml @@ -125,12 +125,17 @@ <artifactId>nifi-registry-framework</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> - <!-- This will directly in lib so mark it as provided --> <dependency> <groupId>org.apache.nifi.registry</groupId> <artifactId>nifi-registry-properties</artifactId> <version>0.0.1-SNAPSHOT</version> - <scope>provided</scope> + <scope>provided</scope> <!-- This will be in the lib directory --> + </dependency> + <dependency> + <groupId>org.apache.nifi.registry</groupId> + <artifactId>nifi-registry-security-api</artifactId> + <version>0.0.1-SNAPSHOT</version> + <scope>provided</scope> <!-- This will be in lib directory --> </dependency> <dependency> <groupId>org.apache.commons</groupId> @@ -165,5 +170,25 @@ <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> </dependency> + <dependency> + <groupId>io.jsonwebtoken</groupId> + <artifactId>jjwt</artifactId> + <version>0.7.0</version> + </dependency> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <version>18.0</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk15on</artifactId> + <version>1.55</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk15on</artifactId> + <version>1.55</version> + </dependency> </dependencies> </project> http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java index 969f849..59452f9 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java @@ -16,13 +16,24 @@ */ package org.apache.nifi.registry.web; +import org.apache.nifi.registry.web.api.AccessPolicyResource; +import org.apache.nifi.registry.web.api.AccessResource; import org.apache.nifi.registry.web.api.BucketFlowResource; import org.apache.nifi.registry.web.api.BucketResource; import org.apache.nifi.registry.web.api.FlowResource; import org.apache.nifi.registry.web.api.ItemResource; +import org.apache.nifi.registry.web.api.ResourceResource; +import org.apache.nifi.registry.web.api.TenantResource; +import org.apache.nifi.registry.web.mapper.AccessDeniedExceptionMapper; +import org.apache.nifi.registry.web.mapper.AdministrationExceptionMapper; +import org.apache.nifi.registry.web.mapper.AuthenticationCredentialsNotFoundExceptionMapper; +import org.apache.nifi.registry.web.mapper.AuthorizationAccessExceptionMapper; import org.apache.nifi.registry.web.mapper.IllegalArgumentExceptionMapper; import org.apache.nifi.registry.web.mapper.IllegalStateExceptionMapper; +import org.apache.nifi.registry.web.mapper.InvalidAuthenticationExceptionMapper; +import org.apache.nifi.registry.web.mapper.NotFoundExceptionMapper; import org.apache.nifi.registry.web.mapper.ResourceNotFoundExceptionMapper; +import org.apache.nifi.registry.web.mapper.SerializationExceptionMapper; import org.apache.nifi.registry.web.mapper.ThrowableMapper; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; @@ -48,17 +59,28 @@ public class NiFiRegistryResourceConfig extends ResourceConfig { // register filters register(HttpMethodOverrideFilter.class); - // register the exception mappers + // register the exception mappers - TODO, see if these can be registered via scanning + register(new AccessDeniedExceptionMapper()); + register(new AdministrationExceptionMapper()); + register(new AuthenticationCredentialsNotFoundExceptionMapper()); + register(new AuthorizationAccessExceptionMapper()); register(new IllegalArgumentExceptionMapper()); register(new IllegalStateExceptionMapper()); + register(new InvalidAuthenticationExceptionMapper()); + register(new NotFoundExceptionMapper()); register(new ResourceNotFoundExceptionMapper()); + register(new SerializationExceptionMapper()); register(new ThrowableMapper()); // register endpoints + register(AccessPolicyResource.class); + register(AccessResource.class); register(BucketResource.class); register(BucketFlowResource.class); - register(FlowResource.class); register(ItemResource.class); + register(FlowResource.class); + register(ResourceResource.class); + register(TenantResource.class); property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true); } http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistrySecurityConfig.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistrySecurityConfig.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistrySecurityConfig.java new file mode 100644 index 0000000..e9431d0 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistrySecurityConfig.java @@ -0,0 +1,220 @@ +/* + * 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; + +import org.apache.nifi.registry.authorization.AuthorizableLookup; +import org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.AuthorizerFactory; +import org.apache.nifi.registry.authorization.StandardAuthorizableLookup; +import org.apache.nifi.registry.authorization.StandardAuthorizerFactory; +import org.apache.nifi.registry.properties.NiFiRegistryProperties; +import org.apache.nifi.registry.web.security.NiFiAnonymousUserFilter; +import org.apache.nifi.registry.web.security.x509.SubjectDnX509PrincipalExtractor; +import org.apache.nifi.registry.web.security.x509.X509AuthenticationFilter; +import org.apache.nifi.registry.web.security.x509.X509AuthenticationProvider; +import org.apache.nifi.registry.web.security.x509.X509CertificateExtractor; +import org.apache.nifi.registry.web.security.x509.X509CertificateValidator; +import org.apache.nifi.registry.web.security.x509.X509IdentityProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.AnonymousAuthenticationFilter; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; + +/** + * NiFi Web Api Spring security + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class NiFiRegistrySecurityConfig extends WebSecurityConfigurerAdapter { + private static final Logger logger = LoggerFactory.getLogger(NiFiRegistrySecurityConfig.class); + + private NiFiRegistryProperties properties; + + private X509AuthenticationFilter x509AuthenticationFilter; + +// private JwtAuthenticationFilter jwtAuthenticationFilter; +// private JwtAuthenticationProvider jwtAuthenticationProvider; +// +// private OtpAuthenticationFilter otpAuthenticationFilter; +// private OtpAuthenticationProvider otpAuthenticationProvider; + + private NiFiAnonymousUserFilter anonymousAuthenticationFilter; + + public NiFiRegistrySecurityConfig() { + super(true); // disable defaults + } + + @Override + public void configure(WebSecurity webSecurity) throws Exception { + // ignore the access endpoints for obtaining the access config, the access token + // granting, and access status for a given user (note: we are not ignoring the + // the /access/download-token and /access/ui-extension-token endpoints + webSecurity + .ignoring() + .antMatchers("/access", "/access/config", "/access/token", "/access/kerberos", "/access/oidc/**"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .rememberMe().disable() + .authorizeRequests() + .anyRequest().fullyAuthenticated() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + // x509 + http.addFilterBefore(x509AuthenticationFilter(), AnonymousAuthenticationFilter.class); + + // jwt + // http.addFilterBefore(jwtFilterBean(), AnonymousAuthenticationFilter.class); + + // otp + // http.addFilterBefore(otpFilterBean(), AnonymousAuthenticationFilter.class); + + // anonymous + http.anonymous().authenticationFilter(anonymousFilter()); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + // override xxxBean method so the authentication manager is available in app context (necessary for the method level security) + return super.authenticationManagerBean(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .authenticationProvider(x509AuthenticationProvider()); + /* TODO, add Jwt and Otp support */ +// .authenticationProvider(jwtAuthenticationProvider) +// .authenticationProvider(otpAuthenticationProvider); + } + +// @Bean // TODO JwtAuthenticationFilter +// public JwtAuthenticationFilter jwtFilterBean() throws Exception { +// if (jwtAuthenticationFilter == null) { +// jwtAuthenticationFilter = new JwtAuthenticationFilter(); +// jwtAuthenticationFilter.setProperties(properties); +// jwtAuthenticationFilter.setAuthenticationManager(authenticationManager()); +// } +// return jwtAuthenticationFilter; +// } +// +// @Bean // TODO OtpAuthenticationFilter +// public OtpAuthenticationFilter otpFilterBean() throws Exception { +// if (otpAuthenticationFilter == null) { +// otpAuthenticationFilter = new OtpAuthenticationFilter(); +// otpAuthenticationFilter.setProperties(properties); +// otpAuthenticationFilter.setAuthenticationManager(authenticationManager()); +// } +// return otpAuthenticationFilter; +// } + + @Bean + public X509AuthenticationFilter x509AuthenticationFilter() throws Exception { + if (x509AuthenticationFilter == null) { + x509AuthenticationFilter = new X509AuthenticationFilter(); + x509AuthenticationFilter.setProperties(properties); + x509AuthenticationFilter.setCertificateExtractor(certificateExtractor()); + x509AuthenticationFilter.setPrincipalExtractor(principalExtractor()); + x509AuthenticationFilter.setAuthenticationManager(authenticationManager()); + } + return x509AuthenticationFilter; + } + + @Bean + public NiFiAnonymousUserFilter anonymousFilter() throws Exception { + if (anonymousAuthenticationFilter == null) { + anonymousAuthenticationFilter = new NiFiAnonymousUserFilter(); + } + return anonymousAuthenticationFilter; + } + + @Bean + public X509CertificateExtractor certificateExtractor() { + return new X509CertificateExtractor(); + } + + public X509CertificateValidator certificateValidator() { + return new X509CertificateValidator(); + } + + @Bean + public X509PrincipalExtractor principalExtractor() { + return new SubjectDnX509PrincipalExtractor(); + } + + @Bean + public X509IdentityProvider x509IdentityProvider() { + X509IdentityProvider x509IdentityProvider = new X509IdentityProvider(); + x509IdentityProvider.setCertificateValidator(certificateValidator()); + x509IdentityProvider.setPrincipalExtractor(principalExtractor()); + return x509IdentityProvider; + } + + @Bean + public X509AuthenticationProvider x509AuthenticationProvider() { + return new X509AuthenticationProvider(x509IdentityProvider(), authorizer(), this.properties); + } + + @Bean(initMethod = "initialize") + public AuthorizerFactory authorizerFactory() { + return new StandardAuthorizerFactory(this.properties); + } + + @Bean + public Authorizer authorizer() { + return authorizerFactory().getAuthorizer(); + } + + @Bean + public AuthorizableLookup authorizableLookup() { + return new StandardAuthorizableLookup(); + } + + @Autowired + public void setProperties(NiFiRegistryProperties properties) { + this.properties = properties; + } + + // TODO, add Jwt and Otp support +// @Autowired +// public void setJwtAuthenticationProvider(JwtAuthenticationProvider jwtAuthenticationProvider) { +// this.jwtAuthenticationProvider = jwtAuthenticationProvider; +// } +// +// @Autowired +// public void setOtpAuthenticationProvider(OtpAuthenticationProvider otpAuthenticationProvider) { +// this.otpAuthenticationProvider = otpAuthenticationProvider; +// } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java new file mode 100644 index 0000000..d72aa94 --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java @@ -0,0 +1,344 @@ +/* + * 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 javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.AuthorizerCapabilityDetection; +import org.apache.nifi.registry.authorization.RequestAction; +import org.apache.nifi.registry.authorization.resource.Authorizable; +import org.apache.nifi.registry.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.model.authorization.AccessPolicy; +import org.apache.nifi.registry.model.authorization.AccessPolicySummary; +import org.apache.nifi.registry.service.AuthorizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.net.URI; +import java.util.List; + +/** + * RESTful endpoint for managing access policies. + */ +@Component +@Path("/policies") +@Api( + value = "/policies", + description = "Endpoint for managing access policies." +) +public class AccessPolicyResource extends AuthorizableApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(AccessPolicyResource.class); + + @Autowired + public AccessPolicyResource( + Authorizer authorizer, + AuthorizationService authorizationService) { + super(authorizer, authorizationService); + } + + // TODO - Verify that access control is done by the resource the policy is for, not the /policies resource itself. + + /** + * Create a new access policy. + * + * @param httpServletRequest request + * @param requestAccessPolicy the access policy to create. + * @return The created access policy. + */ + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Creates an access policy", + response = AccessPolicy.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createAccessPolicy( + @Context final HttpServletRequest httpServletRequest, + @ApiParam(value = "The access policy configuration details.", required = true) + final AccessPolicy requestAccessPolicy) { + + verifyAuthorizerSupportsConfigurablePolicies(); + + if (requestAccessPolicy == null) { + throw new IllegalArgumentException("Access policy details must be specified when creating a new policy."); + } + if (requestAccessPolicy.getIdentifier() != null) { + throw new IllegalArgumentException("Access policy ID cannot be specified when creating a new policy."); + } + if (requestAccessPolicy.getResource() == null) { + throw new IllegalArgumentException("Resource must be specified when creating a new access policy."); + } + RequestAction.valueOfValue(requestAccessPolicy.getAction()); + + authorizeAccessToResource(RequestAction.WRITE, requestAccessPolicy.getResource()); + + AccessPolicy createdPolicy = authorizationService.createAccessPolicy(requestAccessPolicy); + + String locationUri = generateAccessPolicyUri(createdPolicy); + return generateCreatedResponse(URI.create(locationUri), createdPolicy).build(); + } + + /** + * Retrieves all access policies + * + * @return A list of access policies + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Gets all access policies", + response = AccessPolicy.class, + responseContainer = "List" + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_409), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessPolicies() { + + verifyAuthorizerIsManaged(); + authorizeAccess(RequestAction.READ); + + final List<AccessPolicy> accessPolicies = authorizationService.getAccessPolicies(); + + return generateOkResponse(accessPolicies).build(); + } + + /** + * Retrieves the specified access policy. + * + * @param identifier The id of the access policy to retrieve + * @return An accessPolicyEntity. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Gets an access policy", + response = AccessPolicy.class + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) + public Response getAccessPolicy( + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") final String identifier) { + + verifyAuthorizerIsManaged(); + + final AccessPolicy accessPolicy = authorizationService.getAccessPolicy(identifier); + authorizeAccessToResource(RequestAction.READ, accessPolicy.getResource()); + + return generateOkResponse(accessPolicy).build(); + } + + + /** + * Retrieve a specified access policy for a given (action, resource) pair. + * + * @param action the action, i.e. "read", "write" + * @param rawResource the name of the resource as a raw string + * @return An access policy. + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{action}/{resource: .+}") + @ApiOperation( + value = "Gets an access policy for the specified action and resource", + notes = "Will return the effective policy if no specific policy exists for the specified action and resource. " + + "Must have Read permissions to the policy with the desired action and resource. Permissions for the policy that is " + + "returned will be indicated in the response. If the client does not have permissions to that policy, the response " + + "will not include the policy and the permissions in the response will be marked accordingly. If the client does " + + "not have permissions to the policy of the desired action and resource a 403 response will be returned.", + response = AccessPolicy.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessPolicyForResource( + @ApiParam(value = "The request action.", allowableValues = "read, write" /* todo, +delete */, required = true) + @PathParam("action") + final String action, + @ApiParam(value = "The resource of the policy.", required = true) + @PathParam("resource") + final String rawResource) { + + verifyAuthorizerIsManaged(); + + // parse the action and resource type + final RequestAction requestAction = RequestAction.valueOfValue(action); + final String resource = "/" + rawResource; + + authorizeAccessToResource(RequestAction.READ, resource); + + AccessPolicy accessPolicy = authorizationService.getAccessPolicy(resource, requestAction); + return generateOkResponse(accessPolicy).build(); + } + + + /** + * Update an access policy. + * + * @param httpServletRequest request + * @param identifier The id of the access policy to update. + * @param requestAccessPolicy An access policy. + * @return the updated access policy. + */ + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Updates a access policy", + response = AccessPolicy.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateAccessPolicy( + @Context + final HttpServletRequest httpServletRequest, + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") + final String identifier, + @ApiParam(value = "The access policy configuration details.", required = true) + final AccessPolicy requestAccessPolicy) { + + verifyAuthorizerSupportsConfigurablePolicies(); + + if (requestAccessPolicy == null) { + throw new IllegalArgumentException("Access policy details must be specified when updating a policy."); + } + if (!identifier.equals(requestAccessPolicy.getIdentifier())) { + throw new IllegalArgumentException(String.format("The policy id in the request body (%s) does not equal the " + + "policy id of the requested resource (%s).", requestAccessPolicy.getIdentifier(), identifier)); + } + + authorizeAccessToPolicy(RequestAction.WRITE, identifier); + + AccessPolicy createdPolicy = authorizationService.createAccessPolicy(requestAccessPolicy); + + String locationUri = generateAccessPolicyUri(createdPolicy); + return generateCreatedResponse(URI.create(locationUri), createdPolicy).build(); + } + + + /** + * Remove a specified access policy. + * + * @param httpServletRequest request + * @param identifier The id of the access policy to remove. + * @return The deleted access policy + */ + @DELETE + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("{id}") + @ApiOperation( + value = "Deletes an access policy", + response = AccessPolicy.class + ) + @ApiResponses({ + @ApiResponse(code = 401, message = "Client could not be authenticated."), + @ApiResponse(code = 403, message = "Client is not authorized to make this request."), + @ApiResponse(code = 404, message = "The specified resource could not be found.") }) + public Response removeAccessPolicy( + @Context final HttpServletRequest httpServletRequest, + @ApiParam(value = "The access policy id.", required = true) + @PathParam("id") + final String identifier) { + + verifyAuthorizerSupportsConfigurablePolicies(); + authorizeAccessToPolicy(RequestAction.WRITE, identifier); + AccessPolicy deletedPolicy = authorizationService.deleteAccessPolicy(identifier); + return generateOkResponse(deletedPolicy).build(); + } + + + private void verifyAuthorizerIsManaged() { + if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) { + throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER); + } + } + + private void verifyAuthorizerSupportsConfigurablePolicies() { + if (!AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer)) { + throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES); + } + } + + private void authorizeAccess(RequestAction actionType) { + authorizationService.authorizeAccess(lookup -> { + final Authorizable policiesAuthorizable = lookup.getPoliciesAuthorizable(); + policiesAuthorizable.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); + }); + } + + private void authorizeAccessToPolicy(RequestAction actionType, String accessPolicyIdentifier) { + AccessPolicy accessPolicy = authorizationService.getAccessPolicy(accessPolicyIdentifier); + authorizeAccessToResource(actionType, accessPolicy.getResource()); + } + + private void authorizeAccessToResource(RequestAction actionType, String resource) { + authorizationService.authorizeAccess(lookup -> { + final Authorizable accessPolicy = lookup.getAccessPolicyByResource(resource); + accessPolicy.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); + }); + } + + private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) { + return generateResourceUri("policies", accessPolicy.getIdentifier()); + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java new file mode 100644 index 0000000..a5c849a --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessResource.java @@ -0,0 +1,164 @@ +/* + * 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.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.apache.commons.lang3.NotImplementedException; +import org.apache.nifi.registry.authorization.exception.AccessDeniedException; +import org.apache.nifi.registry.authorization.user.NiFiUser; +import org.apache.nifi.registry.authorization.user.NiFiUserDetails; +import org.apache.nifi.registry.exception.AdministrationException; +import org.apache.nifi.registry.model.authorization.AccessStatus; +import org.apache.nifi.registry.web.security.InvalidAuthenticationException; +import org.apache.nifi.registry.web.security.ProxiedEntitiesUtils; +import org.apache.nifi.registry.web.security.UntrustedProxyException; +import org.apache.nifi.registry.web.security.token.NiFiAuthenticationToken; +import org.apache.nifi.registry.web.security.x509.X509AuthenticationProvider; +import org.apache.nifi.registry.web.security.x509.X509AuthenticationRequestToken; +import org.apache.nifi.registry.web.security.x509.X509CertificateExtractor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.security.cert.X509Certificate; + +@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 X509CertificateExtractor certificateExtractor; + private X509AuthenticationProvider x509AuthenticationProvider; + private X509PrincipalExtractor x509principalExtractor; + + public AccessResource(X509CertificateExtractor certificateExtractor, X509AuthenticationProvider x509AuthenticationProvider, X509PrincipalExtractor x509principalExtractor) { + this.certificateExtractor = certificateExtractor; + this.x509AuthenticationProvider = x509AuthenticationProvider; + this.x509principalExtractor = x509principalExtractor; + } + + /** + * Gets the status the client's access. + * + * @param httpServletRequest the servlet request + * @return A accessStatusEntity + */ + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @Path("") + @ApiOperation( + value = "Gets the status the client's access", + response = AccessStatus.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getAccessStatus(@Context HttpServletRequest httpServletRequest) { + // only consider user specific access over https + if (!httpServletRequest.isSecure()) { + throw new IllegalStateException("User authentication/authorization is only supported when running over HTTPS."); + } + + final AccessStatus accessStatus = new AccessStatus(); + + try { + final X509Certificate[] certificates = certificateExtractor.extractClientCertificate(httpServletRequest); + + // if there is not certificate, consider a token + if (certificates == null) { + + // TODO - add JWT Authentication support + throw new NotImplementedException("NiFi Registry client is trying to authentication with something other than a client cert. " + + "At this time, only client certificate authentication is supported."); + +// // look for an authorization token +// final String authorization = httpServletRequest.getHeader(JwtAuthenticationFilter.AUTHORIZATION); +// +// // if there is no authorization header, we don't know the user +// if (authorization == null) { +// accessStatus.setStatus(AccessStatus.Status.UNKNOWN.name()); +// accessStatus.setMessage("No credentials supplied, unknown user."); +// } else { +// try { +// // Extract the Base64 encoded token from the Authorization header +// final String token = StringUtils.substringAfterLast(authorization, " "); +// +// final JwtAuthenticationRequestToken jwtRequest = new JwtAuthenticationRequestToken(token, httpServletRequest.getRemoteAddr()); +// final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) jwtAuthenticationProvider.authenticate(jwtRequest); +// final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser(); +// +// // set the user identity +// accessStatus.setIdentity(nifiUser.getIdentity()); +// +// // attempt authorize to /flow +// accessStatus.setStatus(AccessStatus.Status.ACTIVE.name()); +// accessStatus.setMessage("You are already logged in."); +// } catch (JwtException e) { +// throw new InvalidAuthenticationException(e.getMessage(), e); +// } +// } + } else { + try { + final X509AuthenticationRequestToken x509Request = new X509AuthenticationRequestToken( + httpServletRequest.getHeader(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN), x509principalExtractor, certificates, httpServletRequest.getRemoteAddr()); + + final NiFiAuthenticationToken authenticationResponse = (NiFiAuthenticationToken) x509AuthenticationProvider.authenticate(x509Request); + final NiFiUser nifiUser = ((NiFiUserDetails) authenticationResponse.getDetails()).getNiFiUser(); + + // set the user identity + accessStatus.setIdentity(nifiUser.getIdentity()); + + // attempt authorize to /flow + accessStatus.setStatus(AccessStatus.Status.ACTIVE.name()); + accessStatus.setMessage("You are already logged in."); + } catch (final IllegalArgumentException iae) { + throw new InvalidAuthenticationException(iae.getMessage(), iae); + } + } + } catch (final UntrustedProxyException upe) { + throw new AccessDeniedException(upe.getMessage(), upe); + } catch (final AuthenticationServiceException ase) { + throw new AdministrationException(ase.getMessage(), ase); + } + + return generateOkResponse(accessStatus).build(); + } + + + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java new file mode 100644 index 0000000..6c4c3ae --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java @@ -0,0 +1,175 @@ +/* + * 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 org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.CacheControl; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriBuilderException; +import javax.ws.rs.core.UriInfo; +import java.net.URI; +import java.net.URISyntaxException; + +public class ApplicationResource { + + public static final String PROXY_SCHEME_HTTP_HEADER = "X-ProxyScheme"; + public static final String PROXY_HOST_HTTP_HEADER = "X-ProxyHost"; + public static final String PROXY_PORT_HTTP_HEADER = "X-ProxyPort"; + public static final String PROXY_CONTEXT_PATH_HTTP_HEADER = "X-ProxyContextPath"; + + public static final String FORWARDED_PROTO_HTTP_HEADER = "X-Forwarded-Proto"; + public static final String FORWARDED_HOST_HTTP_HEADER = "X-Forwarded-Server"; + public static final String FORWARDED_PORT_HTTP_HEADER = "X-Forwarded-Port"; + public static final String FORWARDED_CONTEXT_HTTP_HEADER = "X-Forwarded-Context"; + + protected static final String NON_GUARANTEED_ENDPOINT = "Note: This endpoint is subject to change as NiFi and its REST API evolve."; + + private static final Logger logger = LoggerFactory.getLogger(ApplicationResource.class); + + @Context + private HttpServletRequest httpServletRequest; + + @Context + private UriInfo uriInfo; + + protected String generateResourceUri(final String... path) { + final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); + uriBuilder.segment(path); + URI uri = uriBuilder.build(); + try { + + // check for proxy settings + final String scheme = getFirstHeaderValue(PROXY_SCHEME_HTTP_HEADER, FORWARDED_PROTO_HTTP_HEADER); + final String host = getFirstHeaderValue(PROXY_HOST_HTTP_HEADER, FORWARDED_HOST_HTTP_HEADER); + final String port = getFirstHeaderValue(PROXY_PORT_HTTP_HEADER, FORWARDED_PORT_HTTP_HEADER); + String baseContextPath = getFirstHeaderValue(PROXY_CONTEXT_PATH_HTTP_HEADER, FORWARDED_CONTEXT_HTTP_HEADER); + + // if necessary, prepend the context path + String resourcePath = uri.getPath(); + if (baseContextPath != null) { + // normalize context path + if (!baseContextPath.startsWith("/")) { + baseContextPath = "/" + baseContextPath; + } + + if (baseContextPath.endsWith("/")) { + baseContextPath = StringUtils.substringBeforeLast(baseContextPath, "/"); + } + + // determine the complete resource path + resourcePath = baseContextPath + resourcePath; + } + + // determine the port uri + int uriPort = uri.getPort(); + if (port != null) { + if (StringUtils.isWhitespace(port)) { + uriPort = -1; + } else { + try { + uriPort = Integer.parseInt(port); + } catch (final NumberFormatException nfe) { + logger.warn(String.format("Unable to parse proxy port HTTP header '%s'. Using port from request URI '%s'.", port, uriPort)); + } + } + } + + // construct the URI + uri = new URI( + (StringUtils.isBlank(scheme)) ? uri.getScheme() : scheme, + uri.getUserInfo(), + (StringUtils.isBlank(host)) ? uri.getHost() : host, + uriPort, + resourcePath, + uri.getQuery(), + uri.getFragment()); + + } catch (final URISyntaxException use) { + throw new UriBuilderException(use); + } + return uri.toString(); + } + + /** + * Edit the response headers to indicating no caching. + * + * @param response response + * @return builder + */ + protected Response.ResponseBuilder noCache(final Response.ResponseBuilder response) { + final CacheControl cacheControl = new CacheControl(); + cacheControl.setPrivate(true); + cacheControl.setNoCache(true); + cacheControl.setNoStore(true); + return response.cacheControl(cacheControl); + } + + /** + * Generates an OK response with the specified content. + * + * @param entity The entity + * @return The response to be built + */ + protected Response.ResponseBuilder generateOkResponse(final Object entity) { + final Response.ResponseBuilder response = Response.ok(entity); + return noCache(response); + } + + /** + * Generates a 201 Created response with the specified content. + * + * @param uri The URI + * @param entity entity + * @return The response to be built + */ + protected Response.ResponseBuilder generateCreatedResponse(final URI uri, final Object entity) { + // generate the response builder + return Response.created(uri).entity(entity); + } + + /** + * Returns the value for the first key discovered when inspecting the current request. Will + * return null if there are no keys specified or if none of the specified keys are found. + * + * @param keys http header keys + * @return the value for the first key found + */ + private String getFirstHeaderValue(final String... keys) { + if (keys == null) { + return null; + } + + for (final String key : keys) { + final String value = httpServletRequest.getHeader(key); + + // if we found an entry for this key, return the value + if (value != null) { + return value; + } + } + + // unable to find any matching keys + return null; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java new file mode 100644 index 0000000..d15685d --- /dev/null +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AuthorizableApplicationResource.java @@ -0,0 +1,82 @@ +/* + * 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 org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.RequestAction; +import org.apache.nifi.registry.authorization.resource.Authorizable; +import org.apache.nifi.registry.authorization.resource.ResourceType; +import org.apache.nifi.registry.authorization.user.NiFiUserUtils; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.model.authorization.Resource; +import org.apache.nifi.registry.service.AuthorizationService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +public class AuthorizableApplicationResource extends ApplicationResource { + + private static final Logger logger = LoggerFactory.getLogger(AuthorizableApplicationResource.class); + + protected final AuthorizationService authorizationService; + protected final Authorizer authorizer; + + protected AuthorizableApplicationResource( + Authorizer authorizer, + AuthorizationService authorizationService) { + this.authorizer = authorizer; + this.authorizationService = authorizationService; + } + + protected void authorizeBucketAccess(RequestAction actionType, String bucketIdentifier) { + authorizationService.authorizeAccess(lookup -> { + final Authorizable bucketAccessPolicy = lookup.getBucketAuthorizable(bucketIdentifier); + bucketAccessPolicy.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); + }); + } + + protected void authorizeBucketItemAccess(RequestAction actionType, BucketItem bucketItem) { + authorizeBucketAccess(actionType, bucketItem.getBucketIdentifier()); + } + + protected Set<String> getAuthorizedBucketIds() { + return authorizationService + .getAuthorizedResources(RequestAction.READ, ResourceType.Bucket) + .stream() + .map(AuthorizableApplicationResource::extractBucketIdFromResource) + .filter(Objects::nonNull) + .distinct() + .collect(Collectors.toSet()); + } + + private static String extractBucketIdFromResource(Resource resource) { + + if (resource == null || resource.getIdentifier() == null || !resource.getIdentifier().startsWith("/buckets/")) { + return null; + } + + String[] pathComponents = resource.getIdentifier().split("/"); + if (pathComponents.length < 3) { + return null; + } + return pathComponents[2]; + } + +} http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java index 7933e87..a42a739 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java @@ -18,20 +18,43 @@ package org.apache.nifi.registry.web.api; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.RequestAction; +import org.apache.nifi.registry.bucket.BucketItem; +import org.apache.nifi.registry.exception.ResourceNotFoundException; import org.apache.nifi.registry.flow.VersionedFlow; +import org.apache.nifi.registry.flow.VersionedFlowSnapshot; +import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata; +import org.apache.nifi.registry.service.AuthorizationService; import org.apache.nifi.registry.service.RegistryService; +import org.apache.nifi.registry.service.params.QueryParameters; +import org.apache.nifi.registry.service.params.SortParameter; +import org.apache.nifi.registry.web.link.LinkService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.util.List; +import java.util.SortedSet; @Component @Path("/buckets/{bucketId}/flows") @@ -39,14 +62,22 @@ import javax.ws.rs.core.Response; value = "bucket >> flows", description = "Create flows scoped to an existing bucket in the registry." ) -public class BucketFlowResource { +public class BucketFlowResource extends AuthorizableApplicationResource { private static final Logger logger = LoggerFactory.getLogger(BucketFlowResource.class); private final RegistryService registryService; + private final LinkService linkService; - public BucketFlowResource(@Autowired final RegistryService registryService) { + @Autowired + public BucketFlowResource( + final RegistryService registryService, + final LinkService linkService, + final AuthorizationService authorizationService, + final Authorizer authorizer) { + super(authorizer, authorizationService); this.registryService = registryService; + this.linkService = linkService; } @POST @@ -57,22 +88,320 @@ public class BucketFlowResource { "The flow id is created by the server and a location URI for the created flow resource is returned.", response = VersionedFlow.class ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) public Response createFlow(@PathParam("bucketId") final String bucketId, final VersionedFlow flow) { + authorizeBucketAccess(RequestAction.WRITE, bucketId); + verifyPathParamsMatchBody(bucketId, flow); final VersionedFlow createdFlow = registryService.createFlow(bucketId, flow); return Response.status(Response.Status.OK).entity(createdFlow).build(); } - /* TODO, add redirection URIs so that GET, PUT, DELETE operations for a given flow id (once created) - * are accessible as a subresource from /buckets as well */ -// @GET -// @PathParam("/{bucketId}/flows/{flowSubpath}") -// @ApiOperation("Redirects to /flows/{flowSubpath}") -// public Response getFlowAlias( -// @PathParam("bucketId") String bucketId, -// @PathParam("flowSubpath") String flowSubpath) { -// logger.info("Redirecting flow operation on bucket resource handler to flow resource handler."); -// UriBuilder addressBuilder = uriInfo.getBaseUriBuilder(); -// addressBuilder.path("flows/" + flowSubpath); -// return Response.seeOther(addressBuilder.build()).build(); -// } + @GET + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get metadata for all flows in all buckets that the registry has stored for which the client is authorized. The information about " + + "the versions of each flow should be obtained by requesting a specific flow by id.", + response = VersionedFlow.class, + responseContainer = "List" + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlows( + @PathParam("bucketId") final String bucketId, + @ApiParam(value = SortParameter.API_PARAM_DESCRIPTION, format = "field:order", allowMultiple = true, example = "name:ASC") + @QueryParam("sort") + final List<String> sortParameters) { + authorizeBucketAccess(RequestAction.READ, bucketId); + + final QueryParameters.Builder paramsBuilder = new QueryParameters.Builder(); + for (String sortParam : sortParameters) { + paramsBuilder.addSort(SortParameter.fromString(sortParam)); + } + + final List<VersionedFlow> flows = registryService.getFlows(bucketId); + linkService.populateFlowLinks(flows); + + return Response.status(Response.Status.OK).entity(flows).build(); + } + + @GET + @Path("{flowId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get metadata for an existing flow the registry has stored. If verbose is true, then the metadata " + + "about all snapshots for the flow will also be returned.", + response = VersionedFlow.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlow( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId, + @QueryParam("verbose") @DefaultValue("false") boolean verbose) { + authorizeBucketAccess(RequestAction.READ, bucketId); + + final VersionedFlow flow = registryService.getFlow(bucketId, flowId, verbose); + + linkService.populateFlowLinks(flow); + + if (flow.getSnapshotMetadata() != null) { + linkService.populateSnapshotLinks(flow.getSnapshotMetadata()); + } + + return Response.status(Response.Status.OK).entity(flow).build(); + } + + @PUT + @Path("{flowId}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Update an existing flow the registry has stored.", + response = VersionedFlow.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response updateFlow( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId, + final VersionedFlow flow) { + + verifyPathParamsMatchBody(bucketId, flowId, flow); + setBucketItemMetadataIfMissing(bucketId, flowId, flow); + + authorizeBucketAccess(RequestAction.WRITE, bucketId); + + final VersionedFlow updatedFlow = registryService.updateFlow(flow); + return Response.status(Response.Status.OK).entity(updatedFlow).build(); + } + + @DELETE + @Path("{flowId}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Delete an existing flow the registry has stored.", + response = VersionedFlow.class + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response deleteFlow( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId) { + + authorizeBucketAccess(RequestAction.WRITE, bucketId); + final VersionedFlow deletedFlow = registryService.deleteFlow(bucketId, flowId); + return Response.status(Response.Status.OK).entity(deletedFlow).build(); + } + + @POST + @Path("{flowId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Create the next version of a given flow ID. " + + "The version number is created by the server and a location URI for the created version resource is returned.", + response = VersionedFlowSnapshot.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response createFlowVersion( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId, + final VersionedFlowSnapshot snapshot) { + verifyPathParamsMatchBody(bucketId, flowId, snapshot); + authorizeBucketAccess(RequestAction.WRITE, bucketId); + + setSnaphotMetadataIfMissing(bucketId, flowId, snapshot); + final VersionedFlowSnapshot createdSnapshot = registryService.createFlowSnapshot(snapshot); + return Response.status(Response.Status.OK).entity(createdSnapshot).build(); + } + + @GET + @Path("{flowId}/versions") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get summary of all versions of a flow for a given flow ID.", + response = VersionedFlowSnapshotMetadata.class, + responseContainer = "List" + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersions( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId) { + authorizeBucketAccess(RequestAction.READ, bucketId); + final VersionedFlow flow = registryService.getFlow(bucketId, flowId, true); + + if (flow.getSnapshotMetadata() != null) { + linkService.populateSnapshotLinks(flow.getSnapshotMetadata()); + } + + return Response.status(Response.Status.OK).entity(flow.getSnapshotMetadata()).build(); + } + + @GET + @Path("{flowId}/versions/latest") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get the latest version of a flow for a given flow ID", + response = VersionedFlowSnapshot.class + ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getLatestFlowVersion( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId) { + authorizeBucketAccess(RequestAction.READ, bucketId); + final VersionedFlow flow = registryService.getFlow(bucketId, flowId, true); + + final SortedSet<VersionedFlowSnapshotMetadata> snapshots = flow.getSnapshotMetadata(); + if (snapshots == null || snapshots.size() == 0) { + throw new ResourceNotFoundException("Not flow versions found for flow with id " + flowId); + } + + final VersionedFlowSnapshotMetadata lastSnapshotMetadata = snapshots.last(); + final VersionedFlowSnapshot lastSnapshot = registryService.getFlowSnapshot(bucketId, flowId, lastSnapshotMetadata.getVersion()); + + return Response.status(Response.Status.OK).entity(lastSnapshot).build(); + } + + @GET + @Path("{flowId}/versions/{versionNumber: \\d+}") + @Consumes(MediaType.WILDCARD) + @Produces(MediaType.APPLICATION_JSON) + @ApiOperation( + value = "Get a given version of a flow for a given flow ID", + response = VersionedFlowSnapshot.class + ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) + public Response getFlowVersion( + @PathParam("bucketId") final String bucketId, + @PathParam("flowId") final String flowId, + @PathParam("versionNumber") final Integer versionNumber) { + authorizeBucketAccess(RequestAction.READ, bucketId); + final VersionedFlowSnapshot snapshot = registryService.getFlowSnapshot(bucketId, flowId, versionNumber); + return Response.status(Response.Status.OK).entity(snapshot).build(); + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, BucketItem bodyBucketItem) throws BadRequestException { + if (StringUtils.isBlank(bucketIdParam)) { + throw new BadRequestException("Bucket id path parameter cannot be blank"); + } + + if (bodyBucketItem == null) { + throw new BadRequestException("Object in body cannot be null"); + } + + if (bodyBucketItem.getBucketIdentifier() != null && !bucketIdParam.equals(bodyBucketItem.getBucketIdentifier())) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, BucketItem bodyBucketItem) throws BadRequestException { + verifyPathParamsMatchBody(bucketIdParam, bodyBucketItem); + + if (StringUtils.isBlank(flowIdParam)) { + throw new BadRequestException("Flow id path parameter cannot be blank"); + } + + if (bodyBucketItem.getIdentifier() != null && !flowIdParam.equals(bodyBucketItem.getIdentifier())) { + throw new BadRequestException("Item id in path param must match item id in body"); + } + } + + private static void verifyPathParamsMatchBody(String bucketIdParam, String flowIdParam, VersionedFlowSnapshot flowSnapshot) throws BadRequestException { + if (StringUtils.isBlank(bucketIdParam)) { + throw new BadRequestException("Bucket id path parameter cannot be blank"); + } + + if (StringUtils.isBlank(flowIdParam)) { + throw new BadRequestException("Flow id path parameter cannot be blank"); + } + + if (flowSnapshot == null) { + throw new BadRequestException("VersionedFlowSnapshot cannot be null in body"); + } + + final VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata(); + if (metadata != null && metadata.getBucketIdentifier() != null && !bucketIdParam.equals(metadata.getBucketIdentifier())) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } + if (metadata != null && metadata.getFlowIdentifier() != null && !flowIdParam.equals(metadata.getFlowIdentifier())) { + throw new BadRequestException("Flow id in path param must match flow id in body"); + } + } + + private static void setBucketItemMetadataIfMissing( + @NotNull String bucketIdParam, + @NotNull String bucketItemIdParam, + @NotNull BucketItem bucketItem) { + if (bucketItem.getBucketIdentifier() == null) { + bucketItem.setBucketIdentifier(bucketIdParam); + } + + if (bucketItem.getIdentifier() == null) { + bucketItem.setIdentifier(bucketItemIdParam); + } + } + + private static void setSnaphotMetadataIfMissing( + @NotNull String bucketIdParam, + @NotNull String flowIdParam, + @NotNull VersionedFlowSnapshot flowSnapshot) { + + VersionedFlowSnapshotMetadata metadata = flowSnapshot.getSnapshotMetadata(); + if (metadata == null) { + metadata = new VersionedFlowSnapshotMetadata(); + } + + if (metadata.getBucketIdentifier() == null) { + metadata.setBucketIdentifier(bucketIdParam); + } + + if (metadata.getFlowIdentifier() == null) { + metadata.setFlowIdentifier(flowIdParam); + } + + flowSnapshot.setSnapshotMetadata(metadata); + } } http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/785cb81f/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java ---------------------------------------------------------------------- diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java index 9a9025a..8c8c493 100644 --- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java +++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java @@ -22,7 +22,12 @@ import io.swagger.annotations.ApiParam; import io.swagger.annotations.ApiResponse; import io.swagger.annotations.ApiResponses; import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.registry.authorization.Authorizer; +import org.apache.nifi.registry.authorization.RequestAction; +import org.apache.nifi.registry.authorization.resource.Authorizable; +import org.apache.nifi.registry.authorization.user.NiFiUserUtils; import org.apache.nifi.registry.bucket.Bucket; +import org.apache.nifi.registry.service.AuthorizationService; import org.apache.nifi.registry.service.RegistryService; import org.apache.nifi.registry.service.params.QueryParameters; import org.apache.nifi.registry.service.params.SortParameter; @@ -33,6 +38,7 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import javax.ws.rs.BadRequestException; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; @@ -57,7 +63,7 @@ import java.util.Set; description = "Create named buckets in the registry to store NiFI objects such flows and extensions. " + "Search for and retrieve existing buckets." ) -public class BucketResource { +public class BucketResource extends AuthorizableApplicationResource { private static final Logger logger = LoggerFactory.getLogger(BucketResource.class); @@ -69,7 +75,12 @@ public class BucketResource { private final RegistryService registryService; @Autowired - public BucketResource(final RegistryService registryService, final LinkService linkService) { + public BucketResource( + final RegistryService registryService, + final LinkService linkService, + final AuthorizationService authorizationService, + final Authorizer authorizer) { + super(authorizer, authorizationService); this.registryService = registryService; this.linkService = linkService; } @@ -81,7 +92,12 @@ public class BucketResource { value = "Create a named bucket capable of storing NiFi bucket objects such as flows and extension bundles.", response = Bucket.class ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) }) public Response createBucket(final Bucket bucket) { + authorizeAccess(RequestAction.WRITE); final Bucket createdBucket = registryService.createBucket(bucket); return Response.status(Response.Status.OK).entity(createdBucket).build(); } @@ -95,10 +111,15 @@ public class BucketResource { response = Bucket.class, responseContainer = "List" ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) }) public Response getBuckets( @ApiParam(value = SortParameter.API_PARAM_DESCRIPTION, format = "field:order", allowMultiple = true, example = "name:ASC") @QueryParam("sort") final List<String> sortParameters) { + authorizeAccess(RequestAction.READ); final QueryParameters.Builder paramsBuilder = new QueryParameters.Builder(); for (String sortParam : sortParameters) { @@ -120,13 +141,13 @@ public class BucketResource { "with the set of items in the bucket, but any further children of those items will not be included.", response = Bucket.class ) - @ApiResponses( - value = { - @ApiResponse(code = 404, message = "The specified resource could not be found."), - } - ) + @ApiResponses({ + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) public Response getBucket(@PathParam("bucketId") final String bucketId, @QueryParam("verbose") @DefaultValue("false") boolean verbose) { + authorizeBucketAccess(RequestAction.READ, bucketId); final Bucket bucket = registryService.getBucket(bucketId, verbose); linkService.populateBucketLinks(bucket); @@ -145,28 +166,29 @@ public class BucketResource { value = "Update the metadata for an existing bucket in the registry. Objects stored in the bucket will not be modified.", response = Bucket.class ) - @ApiResponses( - value = { - @ApiResponse(code = 404, message = "The specified resource could not be found."), - } - ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404), + @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) }) public Response updateBucket(@PathParam("bucketId") final String bucketId, final Bucket bucket) { if (StringUtils.isBlank(bucketId)) { - throw new IllegalArgumentException("Bucket Id cannot be blank"); + throw new BadRequestException("Bucket id cannot be blank"); } if (bucket == null) { - throw new IllegalArgumentException("Bucket cannot be null"); + throw new BadRequestException("Bucket cannot be null"); } if (bucket.getIdentifier() != null && !bucketId.equals(bucket.getIdentifier())) { - throw new IllegalArgumentException("Bucket id in path param must match bucket id in body"); - } - - if (bucket.getIdentifier() == null) { + throw new BadRequestException("Bucket id in path param must match bucket id in body"); + } else { bucket.setIdentifier(bucketId); } + authorizeBucketAccess(RequestAction.WRITE, bucketId); + final Bucket updatedBucket = registryService.updateBucket(bucket); return Response.status(Response.Status.OK).entity(updatedBucket).build(); } @@ -179,12 +201,16 @@ public class BucketResource { value = "Delete an existing bucket in the registry, along with all the objects it is storing.", response = Bucket.class ) - @ApiResponses( - value = { - @ApiResponse(code = 404, message = "The specified resource could not be found."), - } - ) + @ApiResponses({ + @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400), + @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401), + @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403), + @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404) }) public Response deleteBucket(@PathParam("bucketId") final String bucketId) { + if (StringUtils.isBlank(bucketId)) { + throw new BadRequestException("Bucket id cannot be blank"); + } + authorizeBucketAccess(RequestAction.WRITE, bucketId); final Bucket deletedBucket = registryService.deleteBucket(bucketId); return Response.status(Response.Status.OK).entity(deletedBucket).build(); } @@ -203,4 +229,11 @@ public class BucketResource { return Response.status(Response.Status.OK).entity(fieldsEntity).build(); } + private void authorizeAccess(RequestAction actionType) { + authorizationService.authorizeAccess(lookup -> { + final Authorizable bucketsAuthorizable = lookup.getBucketsAuthorizable(); + bucketsAuthorizable.authorize(authorizer, actionType, NiFiUserUtils.getNiFiUser()); + }); + } + }
