Java tutorial
/* * 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.apache.nifi.registry.SecureLdapTestApiApplication; import org.apache.nifi.registry.authorization.AccessPolicy; import org.apache.nifi.registry.authorization.AccessPolicySummary; import org.apache.nifi.registry.authorization.CurrentUser; import org.apache.nifi.registry.authorization.Permissions; import org.apache.nifi.registry.authorization.Tenant; import org.apache.nifi.registry.bucket.Bucket; import org.apache.nifi.registry.extension.ExtensionManager; import org.apache.nifi.registry.properties.AESSensitivePropertyProvider; import org.apache.nifi.registry.properties.NiFiRegistryProperties; import org.apache.nifi.registry.properties.SensitivePropertyProvider; import org.apache.nifi.registry.security.authorization.Authorizer; import org.apache.nifi.registry.security.authorization.AuthorizerFactory; import org.apache.nifi.registry.security.crypto.BootstrapFileCryptoKeyProvider; import org.apache.nifi.registry.security.crypto.CryptoKeyProvider; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.skyscreamer.jsonassert.JSONAssert; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.DependsOn; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.test.context.jdbc.Sql; import org.springframework.test.context.junit4.SpringRunner; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * Deploy the Web API Application using an embedded Jetty Server for local integration testing, with the follow characteristics: * * - A NiFiRegistryProperties has to be explicitly provided to the ApplicationContext using a profile unique to this test suite. * - A NiFiRegistryClientConfig has been configured to create a client capable of completing one-way TLS * - The database is embed H2 using volatile (in-memory) persistence * - Custom SQL is clearing the DB before each test method by default, unless method overrides this behavior */ @RunWith(SpringRunner.class) @SpringBootTest(classes = SecureLdapTestApiApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = "spring.profiles.include=ITSecureLdap") @Import(SecureITClientConfiguration.class) @Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql") public class SecureLdapIT extends IntegrationTestBase { private static final String tokenLoginPath = "access/token/login"; private static final String tokenIdentityProviderPath = "access/token/identity-provider"; @TestConfiguration @Profile("ITSecureLdap") public static class LdapTestConfiguration { static AuthorizerFactory authorizerFactory; @Primary @Bean @DependsOn({ "directoryServer" }) // Can't load LdapUserGroupProvider until the embedded LDAP server, which creates the "directoryServer" bean, is running public static Authorizer getAuthorizer(@Autowired NiFiRegistryProperties properties, ExtensionManager extensionManager) throws Exception { if (authorizerFactory == null) { authorizerFactory = new AuthorizerFactory(properties, extensionManager, sensitivePropertyProvider()); } return authorizerFactory.getAuthorizer(); } @Primary @Bean public static SensitivePropertyProvider sensitivePropertyProvider() throws Exception { return new AESSensitivePropertyProvider(getNiFiRegistryMasterKeyProvider().getKey()); } private static CryptoKeyProvider getNiFiRegistryMasterKeyProvider() { return new BootstrapFileCryptoKeyProvider("src/test/resources/conf/secure-ldap/bootstrap.conf"); } } private String adminAuthToken; private List<AccessPolicy> beforeTestAccessPoliciesSnapshot; @Before public void setup() { final String basicAuthCredentials = encodeCredentialsForBasicAuth("nifiadmin", "password"); final String token = client.target(createURL(tokenIdentityProviderPath)).request() .header("Authorization", "Basic " + basicAuthCredentials).post(null, String.class); adminAuthToken = token; beforeTestAccessPoliciesSnapshot = createAccessPoliciesSnapshot(); } @After public void cleanup() { restoreAccessPoliciesSnapshot(beforeTestAccessPoliciesSnapshot); } @Test public void testTokenGenerationAndAccessStatus() throws Exception { // Note: this test intentionally does not use the token generated // for nifiadmin by the @Before method // Given: the client and server have been configured correctly for LDAP authentication String expectedJwtPayloadJson = "{" + "\"sub\":\"nobel\"," + "\"preferred_username\":\"nobel\"," + "\"iss\":\"LdapIdentityProvider\"" + "}"; String expectedAccessStatusJson = "{" + "\"identity\":\"nobel\"," + "\"anonymous\":false" + "}"; // When: the /access/token/login endpoint is queried final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); final Response tokenResponse = client.target(createURL(tokenIdentityProviderPath)).request() .header("Authorization", "Basic " + basicAuthCredentials).post(null, Response.class); // Then: the server returns 200 OK with an access token assertEquals(201, tokenResponse.getStatus()); String token = tokenResponse.readEntity(String.class); assertTrue(StringUtils.isNotEmpty(token)); String[] jwtParts = token.split("\\."); assertEquals(3, jwtParts.length); String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8"); JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); // When: the token is returned in the Authorization header final Response accessResponse = client.target(createURL("access")).request() .header("Authorization", "Bearer " + token).get(Response.class); // Then: the server acknowledges the client has access assertEquals(200, accessResponse.getStatus()); String accessStatus = accessResponse.readEntity(String.class); JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); } @Test public void testTokenGenerationWithIdentityProvider() throws Exception { // Given: the client and server have been configured correctly for LDAP authentication String expectedJwtPayloadJson = "{" + "\"sub\":\"nobel\"," + "\"preferred_username\":\"nobel\"," + "\"iss\":\"LdapIdentityProvider\"," + "\"aud\":\"LdapIdentityProvider\"" + "}"; String expectedAccessStatusJson = "{" + "\"identity\":\"nobel\"," + "\"anonymous\":false" + "}"; // When: the /access/token/identity-provider endpoint is queried final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); final Response tokenResponse = client.target(createURL(tokenIdentityProviderPath)).request() .header("Authorization", "Basic " + basicAuthCredentials).post(null, Response.class); // Then: the server returns 200 OK with an access token assertEquals(201, tokenResponse.getStatus()); String token = tokenResponse.readEntity(String.class); assertTrue(StringUtils.isNotEmpty(token)); String[] jwtParts = token.split("\\."); assertEquals(3, jwtParts.length); String jwtPayload = new String(Base64.getDecoder().decode(jwtParts[1]), "UTF-8"); JSONAssert.assertEquals(expectedJwtPayloadJson, jwtPayload, false); // When: the token is returned in the Authorization header final Response accessResponse = client.target(createURL("access")).request() .header("Authorization", "Bearer " + token).get(Response.class); // Then: the server acknowledges the client has access assertEquals(200, accessResponse.getStatus()); String accessStatus = accessResponse.readEntity(String.class); JSONAssert.assertEquals(expectedAccessStatusJson, accessStatus, false); } @Test public void testGetCurrentUserFailsForAnonymous() throws Exception { // Given: the client is connected to an unsecured NiFi Registry // When: the /access endpoint is queried with no credentials final Response response = client.target(createURL("/access")).request().get(Response.class); // Then: the server returns a 200 OK with the expected current user assertEquals(401, response.getStatus()); } @Test public void testGetCurrentUser() throws Exception { // Given: the client is connected to an unsecured NiFi Registry String expectedJson = "{" + "\"identity\":\"nifiadmin\"," + "\"anonymous\":false," + "\"resourcePermissions\":{" + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}" + "}"; // When: the /access endpoint is queried using a JWT for the nifiadmin LDAP user final Response response = client.target(createURL("/access")).request() .header("Authorization", "Bearer " + adminAuthToken).get(Response.class); // Then: the server returns a 200 OK with the expected current user assertEquals(200, response.getStatus()); String actualJson = response.readEntity(String.class); JSONAssert.assertEquals(expectedJson, actualJson, false); } @Test public void testUsers() throws Exception { // Given: the client and server have been configured correctly for LDAP authentication String expectedJson = "[" + "{\"identity\":\"nifiadmin\",\"userGroups\":[],\"configurable\":false," + "\"resourcePermissions\":{" + "\"anyTopLevelResource\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"buckets\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"tenants\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"policies\":{\"canRead\":true,\"canWrite\":true,\"canDelete\":true}," + "\"proxy\":{\"canRead\":false,\"canWrite\":true,\"canDelete\":false}}}," + "{\"identity\":\"euler\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"euclid\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"boyle\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"newton\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"riemann\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"gauss\",\"userGroups\":[{\"identity\":\"mathematicians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"galileo\",\"userGroups\":[{\"identity\":\"scientists\"},{\"identity\":\"italians\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"nobel\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"pasteur\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"tesla\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"nogroup\",\"userGroups\":[],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"einstein\",\"userGroups\":[{\"identity\":\"scientists\"}],\"accessPolicies\":[],\"configurable\":false}," + "{\"identity\":\"curie\",\"userGroups\":[{\"identity\":\"chemists\"}],\"accessPolicies\":[],\"configurable\":false}]"; // When: the /tenants/users endpoint is queried final String usersJson = client.target(createURL("tenants/users")).request() .header("Authorization", "Bearer " + adminAuthToken).get(String.class); // Then: the server returns a list of all users (see test-ldap-data.ldif) JSONAssert.assertEquals(expectedJson, usersJson, false); } @Test public void testUserGroups() throws Exception { // Given: the client and server have been configured correctly for LDAP authentication String expectedJson = "[" + "{" + "\"identity\":\"chemists\"," + "\"users\":[{\"identity\":\"pasteur\"},{\"identity\":\"boyle\"},{\"identity\":\"curie\"},{\"identity\":\"nobel\"}]," + "\"accessPolicies\":[]," + "\"configurable\":false" + "}," + "{" + "\"identity\":\"mathematicians\"," + "\"users\":[{\"identity\":\"gauss\"},{\"identity\":\"euclid\"},{\"identity\":\"riemann\"},{\"identity\":\"euler\"}]," + "\"accessPolicies\":[]," + "\"configurable\":false" + "}," + "{" + "\"identity\":\"scientists\"," + "\"users\":[{\"identity\":\"einstein\"},{\"identity\":\"tesla\"},{\"identity\":\"newton\"},{\"identity\":\"galileo\"}]," + "\"accessPolicies\":[]," + "\"configurable\":false" + "}," + "{" + "\"identity\":\"italians\"," + "\"users\":[{\"identity\":\"galileo\"}]," + "\"accessPolicies\":[]," + "\"configurable\":false" + "}]"; // When: the /tenants/users endpoint is queried final String groupsJson = client.target(createURL("tenants/user-groups")).request() .header("Authorization", "Bearer " + adminAuthToken).get(String.class); // Then: the server returns a list of all users (see test-ldap-data.ldif) JSONAssert.assertEquals(expectedJson, groupsJson, false); } @Test public void testCreateTenantFails() throws Exception { // Given: the server has been configured with the LdapUserGroupProvider, which is non-configurable, // and: the client wants to create a tenant Tenant tenant = new Tenant(); tenant.setIdentity("new_tenant"); // When: the POST /tenants/users endpoint is accessed final Response createUserResponse = client.target(createURL("tenants/users")).request() .header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); // Then: an error is returned assertEquals(409, createUserResponse.getStatus()); // When: the POST /tenants/users endpoint is accessed final Response createUserGroupResponse = client.target(createURL("tenants/user-groups")).request() .header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(tenant, MediaType.APPLICATION_JSON_TYPE), Response.class); // Then: an error is returned because the UserGroupProvider is non-configurable assertEquals(409, createUserGroupResponse.getStatus()); } @Test public void testAccessPolicyCreation() throws Exception { // Given: the server has been configured with an initial admin "nifiadmin" and a user with no accessPolicies "nobel" String nobelId = getTenantIdentifierByIdentity("nobel"); String chemistsId = getTenantIdentifierByIdentity("chemists"); // a group containing user "nobel" final String basicAuthCredentials = encodeCredentialsForBasicAuth("nobel", "password"); final String nobelAuthToken = client.target(createURL(tokenIdentityProviderPath)).request() .header("Authorization", "Basic " + basicAuthCredentials).post(null, String.class); // When: user nobel re-checks top-level permissions final CurrentUser currentUser = client.target(createURL("/access")).request() .header("Authorization", "Bearer " + nobelAuthToken).get(CurrentUser.class); // Then: 200 OK is returned indicating user has access to no top-level resources assertEquals(new Permissions(), currentUser.getResourcePermissions().getBuckets()); assertEquals(new Permissions(), currentUser.getResourcePermissions().getTenants()); assertEquals(new Permissions(), currentUser.getResourcePermissions().getPolicies()); assertEquals(new Permissions(), currentUser.getResourcePermissions().getProxy()); // When: nifiadmin creates a bucket final Bucket bucket = new Bucket(); bucket.setName("Integration Test Bucket"); bucket.setDescription("A bucket created by an integration test."); Response adminCreatesBucketResponse = client.target(createURL("buckets")).request() .header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(bucket, MediaType.APPLICATION_JSON), Response.class); // Then: the server returns a 200 OK assertEquals(200, adminCreatesBucketResponse.getStatus()); Bucket createdBucket = adminCreatesBucketResponse.readEntity(Bucket.class); // When: user nobel initial queries /buckets final Bucket[] buckets1 = client.target(createURL("buckets")).request() .header("Authorization", "Bearer " + nobelAuthToken).get(Bucket[].class); // Then: an empty list is returned (nobel has no read access yet) assertNotNull(buckets1); assertEquals(0, buckets1.length); // When: nifiadmin grants read access on createdBucket to 'chemists' a group containing nobel AccessPolicy readPolicy = new AccessPolicy(); readPolicy.setResource("/buckets/" + createdBucket.getIdentifier()); readPolicy.setAction("read"); readPolicy.addUserGroups(Arrays.asList(new Tenant(chemistsId, "chemists"))); Response adminGrantsReadAccessResponse = client.target(createURL("policies")).request() .header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(readPolicy, MediaType.APPLICATION_JSON), Response.class); // Then: the server returns a 201 Created assertEquals(201, adminGrantsReadAccessResponse.getStatus()); // When: nifiadmin tries to list all buckets final Bucket[] adminBuckets = client.target(createURL("buckets")).request() .header("Authorization", "Bearer " + adminAuthToken).get(Bucket[].class); // Then: the full list is returned (verifies that per-bucket access policies are additive to base /buckets policy) assertNotNull(adminBuckets); assertEquals(1, adminBuckets.length); assertEquals(createdBucket.getIdentifier(), adminBuckets[0].getIdentifier()); assertEquals(new Permissions().withCanRead(true).withCanWrite(true).withCanDelete(true), adminBuckets[0].getPermissions()); // When: user nobel re-queries /buckets final Bucket[] buckets2 = client.target(createURL("buckets")).request() .header("Authorization", "Bearer " + nobelAuthToken).get(Bucket[].class); // Then: the created bucket is now present assertNotNull(buckets2); assertEquals(1, buckets2.length); assertEquals(createdBucket.getIdentifier(), buckets2[0].getIdentifier()); assertEquals(new Permissions().withCanRead(true), buckets2[0].getPermissions()); // When: nifiadmin grants write access on createdBucket to user 'nobel' AccessPolicy writePolicy = new AccessPolicy(); writePolicy.setResource("/buckets/" + createdBucket.getIdentifier()); writePolicy.setAction("write"); writePolicy.addUsers(Arrays.asList(new Tenant(nobelId, "nobel"))); Response adminGrantsWriteAccessResponse = client.target(createURL("policies")).request() .header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(writePolicy, MediaType.APPLICATION_JSON), Response.class); // Then: the server returns a 201 Created assertEquals(201, adminGrantsWriteAccessResponse.getStatus()); // When: user nobel re-queries /buckets final Bucket[] buckets3 = client.target(createURL("buckets")).request() .header("Authorization", "Bearer " + nobelAuthToken).get(Bucket[].class); // Then: the authorizedActions are updated assertNotNull(buckets3); assertEquals(1, buckets3.length); assertEquals(createdBucket.getIdentifier(), buckets3[0].getIdentifier()); assertEquals(new Permissions().withCanRead(true).withCanWrite(true), buckets3[0].getPermissions()); } /** A helper method to lookup identifiers for tenant identities using the REST API * * @param tenantIdentity - the identity to lookup * @return A string containing the identifier of the tenant, or null if the tenant identity is not found. */ private String getTenantIdentifierByIdentity(String tenantIdentity) { final Tenant[] users = client.target(createURL("tenants/users")).request() .header("Authorization", "Bearer " + adminAuthToken).get(Tenant[].class); final Tenant[] groups = client.target(createURL("tenants/user-groups")).request() .header("Authorization", "Bearer " + adminAuthToken).get(Tenant[].class); final Tenant matchedTenant = Stream.concat(Arrays.stream(users), Arrays.stream(groups)) .filter(tenant -> tenant.getIdentity().equalsIgnoreCase(tenantIdentity)).findFirst().orElse(null); return matchedTenant != null ? matchedTenant.getIdentifier() : null; } /** A helper method to lookup access policies * * @return A string containing the identifier of the policy, or null if the policy identity is not found. */ private AccessPolicy getPolicyByResourceAction(String action, String resource) { final AccessPolicySummary[] policies = client.target(createURL("policies")).request() .header("Authorization", "Bearer " + adminAuthToken).get(AccessPolicySummary[].class); final AccessPolicySummary matchedPolicy = Arrays.stream(policies) .filter(p -> p.getAction().equalsIgnoreCase(action) && p.getResource().equalsIgnoreCase(resource)) .findFirst().orElse(null); if (matchedPolicy == null) { return null; } String policyId = matchedPolicy.getIdentifier(); final AccessPolicy policy = client.target(createURL("policies/" + policyId)).request() .header("Authorization", "Bearer " + adminAuthToken).get(AccessPolicy.class); return policy; } private List<AccessPolicy> createAccessPoliciesSnapshot() { final AccessPolicySummary[] policySummaries = client.target(createURL("policies")).request() .header("Authorization", "Bearer " + adminAuthToken).get(AccessPolicySummary[].class); final List<AccessPolicy> policies = new ArrayList<>(policySummaries.length); for (AccessPolicySummary s : policySummaries) { AccessPolicy policy = client.target(createURL("policies/" + s.getIdentifier())).request() .header("Authorization", "Bearer " + adminAuthToken).get(AccessPolicy.class); policies.add(policy); } return policies; } private void restoreAccessPoliciesSnapshot(List<AccessPolicy> accessPoliciesSnapshot) { List<AccessPolicy> currentAccessPolicies = createAccessPoliciesSnapshot(); Set<String> policiesToRestore = accessPoliciesSnapshot.stream().map(AccessPolicy::getIdentifier) .collect(Collectors.toSet()); Set<String> policiesToDelete = currentAccessPolicies.stream() .filter(p -> !policiesToRestore.contains(p.getIdentifier())).map(AccessPolicy::getIdentifier) .collect(Collectors.toSet()); for (AccessPolicy originalPolicy : accessPoliciesSnapshot) { Response getCurrentPolicy = client.target(createURL("policies/" + originalPolicy.getIdentifier())) .request().header("Authorization", "Bearer " + adminAuthToken).get(Response.class); if (getCurrentPolicy.getStatus() == 200) { // update policy to match original client.target(createURL("policies/" + originalPolicy.getIdentifier())).request() .header("Authorization", "Bearer " + adminAuthToken) .put(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON)); } else { // post the original policy client.target(createURL("policies")).request().header("Authorization", "Bearer " + adminAuthToken) .post(Entity.entity(originalPolicy, MediaType.APPLICATION_JSON)); } } for (String id : policiesToDelete) { try { client.target(createURL("policies/" + id)).request() .header("Authorization", "Bearer " + adminAuthToken).delete(); } catch (Exception e) { // do nothing } } } private static Form encodeCredentialsForURLFormParams(String username, String password) { return new Form().param("username", username).param("password", password); } private static String encodeCredentialsForBasicAuth(String username, String password) { final String credentials = username + ":" + password; final String base64credentials = new String( Base64.getEncoder().encode(credentials.getBytes(Charset.forName("UTF-8")))); return base64credentials; } }