/*
*
* Copyright 2016 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.genie.web.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.netflix.genie.common.dto.Application;
import com.netflix.genie.common.dto.ApplicationStatus;
import com.netflix.genie.common.dto.Cluster;
import com.netflix.genie.common.dto.ClusterStatus;
import com.netflix.genie.common.dto.Command;
import com.netflix.genie.common.dto.CommandStatus;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import java.util.TimeZone;
import java.util.UUID;
/**
* Shared tests for accessing API resources. Any API configuration integration tests should extend this for consistent
* behavior.
*
* @author tgianos
* @since 3.0.0
*/
public abstract class AbstractAPISecurityIntegrationTests {
private static final Application APPLICATION =
new Application.Builder(
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
ApplicationStatus.ACTIVE
).build();
private static final Cluster CLUSTER =
new Cluster.Builder(
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
ClusterStatus.UP
).build();
private static final Command COMMAND =
new Command.Builder(
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
CommandStatus.ACTIVE,
UUID.randomUUID().toString(),
1000L
).build();
private static final ObjectMapper OBJECT_MAPPER;
static {
OBJECT_MAPPER = new ObjectMapper()
.setTimeZone(TimeZone.getTimeZone("UTC"))
.setDateFormat(new ISO8601DateFormat())
.registerModule(new Jdk8Module());
}
private static final String APPLICATIONS_API = "/api/v3/applications";
private static final String CLUSTERS_API = "/api/v3/clusters";
private static final String COMMANDS_API = "/api/v3/commands";
private static final String JOBS_API = "/api/v3/jobs";
private static final ResultMatcher OK = MockMvcResultMatchers.status().isOk();
private static final ResultMatcher BAD_REQUEST = MockMvcResultMatchers.status().isBadRequest();
private static final ResultMatcher CREATED = MockMvcResultMatchers.status().isCreated();
private static final ResultMatcher NO_CONTENT = MockMvcResultMatchers.status().isNoContent();
private static final ResultMatcher NOT_FOUND = MockMvcResultMatchers.status().isNotFound();
private static final ResultMatcher FORBIDDEN = MockMvcResultMatchers.status().isForbidden();
private static final ResultMatcher UNAUTHORIZED = MockMvcResultMatchers.status().isUnauthorized();
@Value("${management.context-path}")
private String actuatorEndpoint;
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
/**
* What ResultMatcher this class should return for unauthorized calls. For example x509 returns 403 while OAuth2
* returns 401 when a call is made while unauthenticated.
*
* @return The result matcher to use when a user isn't logged in
*/
public abstract ResultMatcher getUnauthorizedExpectedStatus();
/**
* Setup for the tests.
*/
@Before
public void setup() {
this.mvc = MockMvcBuilders
.webAppContextSetup(this.context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
/**
* Make sure we can get root.
*
* @throws Exception on any error
*/
@Test
public void canGetRoot() throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.status().isOk());
}
/**
* Make sure we can't call any API if not authenticated.
*
* @throws Exception on any error
*/
@Test
public void cantCallAnyAPIIfUnauthenticated() throws Exception {
final ResultMatcher expectedUnauthenticatedStatus = this.getUnauthorizedExpectedStatus();
this.get(APPLICATIONS_API, expectedUnauthenticatedStatus);
this.get(CLUSTERS_API, expectedUnauthenticatedStatus);
this.get(COMMANDS_API, expectedUnauthenticatedStatus);
this.get(JOBS_API, expectedUnauthenticatedStatus);
this.checkActuatorEndpoints(UNAUTHORIZED);
}
/**
* Make sure we can't call anything under admin control as a regular user.
*
* @throws Exception on any error
*/
@Test
@WithMockUser
public void cantCallAdminAPIsAsRegularUser() throws Exception {
this.get(APPLICATIONS_API, OK);
this.delete(APPLICATIONS_API, FORBIDDEN);
this.post(APPLICATIONS_API, APPLICATION, FORBIDDEN);
this.get(APPLICATIONS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(APPLICATIONS_API + "/" + UUID.randomUUID().toString(), APPLICATION, FORBIDDEN);
this.get(CLUSTERS_API, OK);
this.delete(CLUSTERS_API, FORBIDDEN);
this.post(CLUSTERS_API, CLUSTER, FORBIDDEN);
this.get(CLUSTERS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(CLUSTERS_API + "/" + UUID.randomUUID().toString(), CLUSTER, FORBIDDEN);
this.get(COMMANDS_API, OK);
this.delete(COMMANDS_API, FORBIDDEN);
this.post(COMMANDS_API, COMMAND, FORBIDDEN);
this.get(COMMANDS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(COMMANDS_API + "/" + UUID.randomUUID().toString(), COMMAND, FORBIDDEN);
this.get(JOBS_API, OK);
this.post(JOBS_API, "{\"key\":\"value\"}", BAD_REQUEST);
this.get(JOBS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.delete(JOBS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.checkActuatorEndpoints(FORBIDDEN);
}
/**
* Make sure we get get anything under admin control if we're an admin.
*
* @throws Exception on any error
*/
@Test
@WithMockUser(roles = {"USER", "ADMIN"})
public void canCallAdminAPIsAsAdminUser() throws Exception {
this.get(APPLICATIONS_API, OK);
this.delete(APPLICATIONS_API, NO_CONTENT);
this.post(APPLICATIONS_API, APPLICATION, CREATED);
this.get(APPLICATIONS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(APPLICATIONS_API + "/" + UUID.randomUUID().toString(), APPLICATION, NOT_FOUND);
this.get(CLUSTERS_API, OK);
this.delete(CLUSTERS_API, NO_CONTENT);
this.post(CLUSTERS_API, CLUSTER, CREATED);
this.get(CLUSTERS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(CLUSTERS_API + "/" + UUID.randomUUID().toString(), CLUSTER, NOT_FOUND);
this.get(COMMANDS_API, OK);
this.delete(COMMANDS_API, NO_CONTENT);
this.post(COMMANDS_API, COMMAND, CREATED);
this.get(COMMANDS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.put(COMMANDS_API + "/" + UUID.randomUUID().toString(), COMMAND, NOT_FOUND);
this.get(JOBS_API, OK);
this.post(JOBS_API, "{\"key\":\"value\"}", BAD_REQUEST);
this.get(JOBS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.delete(JOBS_API + "/" + UUID.randomUUID().toString(), NOT_FOUND);
this.checkActuatorEndpoints(OK);
}
private void post(final String endpoint, final Object body, final ResultMatcher expectedStatus) throws Exception {
this.mvc
.perform(
MockMvcRequestBuilders
.post(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(OBJECT_MAPPER.writeValueAsBytes(body))
).andExpect(expectedStatus);
}
private void put(final String endpoint, final Object body, final ResultMatcher expectedStatus) throws Exception {
this.mvc
.perform(
MockMvcRequestBuilders
.put(endpoint)
.contentType(MediaType.APPLICATION_JSON)
.content(OBJECT_MAPPER.writeValueAsBytes(body))
).andExpect(expectedStatus);
}
private void get(final String endpoint, final ResultMatcher expectedStatus) throws Exception {
this.mvc.perform(MockMvcRequestBuilders.get(endpoint)).andExpect(expectedStatus);
}
private void delete(final String endpoint, final ResultMatcher expectedStatus) throws Exception {
this.mvc.perform(MockMvcRequestBuilders.delete(endpoint)).andExpect(expectedStatus);
}
private void checkActuatorEndpoints(final ResultMatcher expectedResult) throws Exception {
// See: https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html
this.get(this.actuatorEndpoint + "/autoconfig", expectedResult);
this.get(this.actuatorEndpoint + "/auditevents", expectedResult);
this.get(this.actuatorEndpoint + "/beans", expectedResult);
this.get(this.actuatorEndpoint + "/configprops", expectedResult);
this.get(this.actuatorEndpoint + "/dump", expectedResult);
this.get(this.actuatorEndpoint + "/env", expectedResult);
this.get(this.actuatorEndpoint + "/health", OK);
this.get(this.actuatorEndpoint + "/info", OK);
this.get(this.actuatorEndpoint + "/loggers", expectedResult);
this.get(this.actuatorEndpoint + "/mappings", expectedResult);
this.get(this.actuatorEndpoint + "/metrics", expectedResult);
this.get(this.actuatorEndpoint + "/trace", expectedResult);
}
}