blob: 8d8ea975cb894459acf3c201c8eabfdb746029a5 [file] [log] [blame]
/*
* 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.NiFiRegistryTestApiApplication;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.kerberos.authentication.KerberosTicketValidation;
import org.springframework.security.kerberos.authentication.KerberosTicketValidator;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit4.SpringRunner;
import javax.ws.rs.core.Response;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Base64;
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(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "spring.profiles.include=ITSecureKerberos")
@Sql(executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:db/clearDB.sql")
public class SecureKerberosIT extends IntegrationTestBase {
private static final String validKerberosTicket = "authenticate_me";
private static final String invalidKerberosTicket = "do_not_authenticate_me";
public static class MockKerberosTicketValidator implements KerberosTicketValidator {
@Override
public KerberosTicketValidation validateTicket(byte[] token) throws BadCredentialsException {
boolean validTicket;
try {
validTicket = Arrays.equals(validKerberosTicket.getBytes("UTF-8"), token);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
if (!validTicket) {
throw new BadCredentialsException(MockKerberosTicketValidator.class.getSimpleName() + " does not validate token");
}
return new KerberosTicketValidation(
"kerberosUser@LOCALHOST",
"HTTP/localhsot@LOCALHOST",
null,
null);
}
}
@Configuration
@Profile("ITSecureKerberos")
@Import({NiFiRegistryTestApiApplication.class, SecureITClientConfiguration.class})
public static class KerberosSpnegoTestConfiguration {
@Primary
@Bean
public static KerberosTicketValidator kerberosTicketValidator() {
return new MockKerberosTicketValidator();
}
}
private String adminAuthToken;
@Before
public void generateAuthToken() {
String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
final String token = client
.target(createURL("/access/token/kerberos"))
.request()
.header("Authorization", "Negotiate " + validTicket)
.post(null, String.class);
adminAuthToken = token;
}
@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 Kerberos SPNEGO authentication
String expectedJwtPayloadJson = "{" +
"\"sub\":\"kerberosUser@LOCALHOST\"," +
"\"preferred_username\":\"kerberosUser@LOCALHOST\"," +
"\"iss\":\"KerberosSpnegoIdentityProvider\"" +
"}";
String expectedAccessStatusJson = "{" +
"\"identity\":\"kerberosUser@LOCALHOST\"," +
"\"anonymous\":false}";
// When: the /access/token/kerberos endpoint is accessed with no credentials
final Response tokenResponse1 = client
.target(createURL("/access/token/kerberos"))
.request()
.post(null, Response.class);
// Then: the server returns 401 Unauthorized with an authenticate challenge header
assertEquals(401, tokenResponse1.getStatus());
assertNotNull(tokenResponse1.getHeaders().get("www-authenticate"));
assertEquals(1, tokenResponse1.getHeaders().get("www-authenticate").size());
assertEquals("Negotiate", tokenResponse1.getHeaders().get("www-authenticate").get(0));
// When: the /access/token/kerberos endpoint is accessed again with an invalid ticket
String invalidTicket = new String(java.util.Base64.getEncoder().encode(invalidKerberosTicket.getBytes(Charset.forName("UTF-8"))));
final Response tokenResponse2 = client
.target(createURL("/access/token/kerberos"))
.request()
.header("Authorization", "Negotiate " + invalidTicket)
.post(null, Response.class);
// Then: the server returns 401 Unauthorized
assertEquals(401, tokenResponse2.getStatus());
// When: the /access/token/kerberos endpoint is accessed with a valid ticket
String validTicket = new String(Base64.getEncoder().encode(validKerberosTicket.getBytes(Charset.forName("UTF-8"))));
final Response tokenResponse3 = client
.target(createURL("/access/token/kerberos"))
.request()
.header("Authorization", "Negotiate " + validTicket)
.post(null, Response.class);
// Then: the server returns 200 OK with a JWT in the body
assertEquals(201, tokenResponse3.getStatus());
String token = tokenResponse3.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 testGetCurrentUser() throws Exception {
// Given: the client is connected to an unsecured NiFi Registry
String expectedJson = "{" +
"\"identity\":\"kerberosUser@LOCALHOST\"," +
"\"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 kerberos 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);
}
}