Skip to content

Commit

Permalink
Merge pull request #224 from gaia-app/59-secure-api-state-endpoint
Browse files Browse the repository at this point in the history
πŸ”’ : secure api state endpoint
  • Loading branch information
cdubuisson authored Feb 5, 2020
2 parents 7c20545 + 3c8981a commit 9f0054b
Show file tree
Hide file tree
Showing 19 changed files with 501 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/state/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/css/**", "/js/**", "/favicon.ico", "/images/**").permitAll()
.antMatchers("/**").authenticated()
Expand All @@ -50,7 +49,7 @@ public void configure(AuthenticationManagerBuilder auth) throws Exception {
// configure default admin user
auth
.inMemoryAuthentication()
.withUser("admin").password(bcrypt().encode(adminPassword)).authorities("ROLE_ADMIN")
.withUser("admin").password(bcrypt().encode(adminPassword)).authorities("ROLE_ADMIN", "ROLE_USER")
.and()
.withUser("user").password(bcrypt().encode("user123")).authorities("ROLE_USER");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package io.codeka.gaia.config.security;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.UUID;

@Configuration
@Order(69)
public class StateApiSecurityConfig extends WebSecurityConfigurerAdapter {

private PasswordEncoder bCrypt;

private static final Log logger = LogFactory.getLog(StateApiSecurityConfig.class);

@Autowired
public StateApiSecurityConfig(PasswordEncoder bCrypt) {
this.bCrypt = bCrypt;
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/state/**")
.authorizeRequests()
.anyRequest().hasAnyRole("STATE", "USER")
.and()
.httpBasic();
}

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
logger.info(String.format("%n%nUsing generated security password for state API: %s%n", properties().getPassword()));

// configure default admin user
auth
.inMemoryAuthentication()
.withUser(properties().getUsername()).password(bCrypt.encode(properties().getPassword())).authorities("ROLE_STATE");
}

@Bean
@ConfigurationProperties(prefix = "gaia.state.api")
public StateApiSecurityProperties properties(){
return new StateApiSecurityProperties("gaia-backend", UUID.randomUUID().toString());
}

public static class StateApiSecurityProperties {

private String password;

private String username;

public StateApiSecurityProperties(String username, String password) {
this.username = username;
this.password = password;
}

public String getUsername() {
return username;
}

public String getPassword() {
return password;
}

}

}
2 changes: 1 addition & 1 deletion src/main/java/io/codeka/gaia/hcl/HclParserImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface HclParser {
fun parseContent(content: String): HclVisitor
fun parseVariables(content: String): List<Variable>
fun parseOutputs(content: String): List<Output>
fun parseProvider(fileContent: String): String
fun parseProvider(content: String): String
}

@Service
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/codeka/gaia/modules/bo/TerraformImage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.codeka.gaia.modules.bo
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Pattern

data class TerraformImage @JvmOverloads constructor(
data class TerraformImage constructor(
@field:NotBlank @field:Pattern(regexp = """^[\w][\w.\-\/]{0,127}$""") val repository: String,
@field:NotBlank val tag: String) {

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/io/codeka/gaia/runner/StackCommandBuilder.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.codeka.gaia.runner;

import com.github.mustachejava.Mustache;
import io.codeka.gaia.config.security.StateApiSecurityConfig;
import io.codeka.gaia.modules.bo.TerraformModule;
import io.codeka.gaia.registries.RegistryOAuth2Provider;
import io.codeka.gaia.settings.bo.Settings;
Expand All @@ -23,14 +24,16 @@
public class StackCommandBuilder {

private Settings settings;
private StateApiSecurityConfig.StateApiSecurityProperties stateApiSecurityProperties;
private Mustache terraformMustache;
private List<RegistryOAuth2Provider> registryOAuth2Providers;

@Autowired
StackCommandBuilder(Settings settings, Mustache terraformMustache, List<RegistryOAuth2Provider> registryOAuth2Providers) {
StackCommandBuilder(Settings settings, Mustache terraformMustache, List<RegistryOAuth2Provider> registryOAuth2Providers, StateApiSecurityConfig.StateApiSecurityProperties stateApiSecurityProperties) {
this.settings = settings;
this.terraformMustache = terraformMustache;
this.registryOAuth2Providers = registryOAuth2Providers;
this.stateApiSecurityProperties = stateApiSecurityProperties;
}

/**
Expand All @@ -53,6 +56,8 @@ private String buildScript(Job job, Stack stack, TerraformModule module,
BiFunction<Stack, TerraformModule, String> command) {
var script = new TerraformScript()
.setExternalUrl(settings.getExternalUrl())
.setStateApiUser(stateApiSecurityProperties.getUsername())
.setStateApiPassword(stateApiSecurityProperties.getPassword())
.setStackId(stack.getId())
.setGitRepositoryUrl(evalGitRepositoryUrl(module))
.setTerraformImage(job.getTerraformImage().image());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public class TerraformScript {
private String externalUrl;
private String stackId;
private String command;
private String stateApiUser;
private String stateApiPassword;

public String getTerraformImage() {
return terraformImage;
Expand Down Expand Up @@ -65,4 +67,22 @@ public TerraformScript setCommand(String command) {
this.command = command;
return this;
}

public String getStateApiUser() {
return stateApiUser;
}

public TerraformScript setStateApiUser(String stateApiUser) {
this.stateApiUser = stateApiUser;
return this;
}

public String getStateApiPassword() {
return stateApiPassword;
}

public TerraformScript setStateApiPassword(String stateApiPassword) {
this.stateApiPassword = stateApiPassword;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public User user(Authentication authentication) {
if (authentication == null) {
return null;
}
if ("gaia-backend".equals(authentication.getName())) {
return null;
}
return userRepository.findById(authentication.getName()).orElseThrow();
}

Expand All @@ -33,6 +36,10 @@ public Team userTeam(Authentication authentication, @ModelAttribute User user) {
if (authentication == null) {
return null;
}
// in case of state access only
if ("gaia-backend".equals(authentication.getName())) {
return null;
}
return user.getTeam();
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/mustache/terraform.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ echo 'generating backend configuration'
echo 'terraform {
backend "http" {
address = "{{externalUrl}}/api/state/{{stackId}}"
username = "{{stateApiUser}}"
password = "{{stateApiPassword}}"
}
}' > backend.tf

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/templates/stack.html
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ <h2>
</div>
<div class="block_content">
<b-form-group label="Name">
<b-input id="var-name"
<b-input id="stack.name"
v-model="stack.name"
:state="stack.name !== ''"
aria-describedby="input-live-help input-live-feedback"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.codeka.gaia.config.security;

import io.codeka.gaia.test.MongoContainer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@SpringBootTest
@AutoConfigureMockMvc
@DirtiesContext
@Testcontainers
public class StateApiSecurityConfigIT {

@Container
private static MongoContainer mongoContainer = new MongoContainer();

@Autowired
private StateApiSecurityConfig.StateApiSecurityProperties props;

@Autowired
private MockMvc mockMvc;

@Test
void gaiaBackend_shouldHaveAccessToStateApi() throws Exception {
mockMvc.perform(get("/api/state/test").with(httpBasic(props.getUsername(), props.getPassword())))
.andExpect(authenticated().withUsername("gaia-backend").withRoles("STATE"));
}

@Test
void gaiaBackend_shouldNotHaveAccessToOtherApis() throws Exception {
mockMvc.perform(get("/api/modules/test").with(httpBasic(props.getUsername(), props.getPassword())))
.andExpect(unauthenticated());
}

@Test
void gaiaBackend_shouldNotHaveAccessToScreens() throws Exception {
mockMvc.perform(get("/modules/test").with(httpBasic(props.getUsername(), props.getPassword())))
.andExpect(unauthenticated());
}


}
15 changes: 15 additions & 0 deletions src/test/java/io/codeka/gaia/e2e/JobPage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.codeka.gaia.e2e

import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.FindBy

class JobPage(webDriver: WebDriver) {

init {
if(! webDriver.currentUrl.contains("/jobs/")){
throw IllegalStateException("This is not the job page. Current page is ${webDriver.currentUrl}")
}
}

}
32 changes: 29 additions & 3 deletions src/test/java/io/codeka/gaia/e2e/SeleniumIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ public class SeleniumIT {
private static MongoContainer mongoContainer = new MongoContainer()
.withScript("src/test/resources/db/00_team.js")
.withScript("src/test/resources/db/10_user.js")
.withScript("src/test/resources/db/20_module.js");
.withScript("src/test/resources/db/20_module.js")
.withScript("src/test/resources/db/30_stack.js")
.withScript("src/test/resources/db/40_job.js")
.withScript("src/test/resources/db/50_step.js")
.withScript("src/test/resources/db/60_terraformState.js");

private static WebDriver driver;

Expand Down Expand Up @@ -75,7 +79,7 @@ private String testUrl(){

@Test
@Order(1) // this test runs first as it logs the user in !
void loginPage() throws IOException {
void loginPage() {
driver.get(testUrl());
driver.manage().window().setSize(new Dimension(1280,800));

Expand All @@ -93,7 +97,7 @@ void dashboardPage_showsModuleCount() throws IOException {
var page = PageFactory.initElements(driver, DashboardPage.class);

assertEquals(3, page.modulesCount());
assertEquals(0, page.stacksCount());
assertEquals(1, page.stacksCount());
assertEquals(0, page.stacksToUpdateCount());

percy.snapshot("Dashboard");
Expand Down Expand Up @@ -123,6 +127,28 @@ void modulePage_showsModuleDetails() throws IOException {
percy.snapshot("Module Details");
}

@Test
void stackPage_showsStackDetails() throws IOException {
driver.get(testUrl()+"/stacks/de28a01f-257a-448d-8e1b-00e4e3a41db2");

var page = new StackPage(driver);
PageFactory.initElements(new AjaxElementLocatorFactory(driver, 10), page);

assertThat(page.stackName()).isEqualTo("local-mongo");

percy.snapshot("Stack Details");
}

@Test
void jobPage_showsJobDetails() throws IOException {
driver.get(testUrl()+"/stacks/de28a01f-257a-448d-8e1b-00e4e3a41db2/jobs/5e856dc7-6bed-465f-abf1-02980206ab2a");

var page = new JobPage(driver);
PageFactory.initElements(new AjaxElementLocatorFactory(driver, 10), page);

percy.snapshot("Job Details");
}

void takeScreenshot() throws IOException {
var file = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
System.out.println(file.getAbsolutePath());
Expand Down
20 changes: 20 additions & 0 deletions src/test/java/io/codeka/gaia/e2e/StackPage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.codeka.gaia.e2e

import org.openqa.selenium.WebDriver
import org.openqa.selenium.WebElement
import org.openqa.selenium.support.FindBy

class StackPage(webDriver: WebDriver) {

init {
if(! webDriver.currentUrl.contains("/stacks/")){
throw IllegalStateException("This is not the stack page. Current page is ${webDriver.currentUrl}")
}
}

@FindBy(id="stack.name")
private lateinit var nameInput: WebElement

fun stackName() : String = nameInput.getAttribute("value")

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.codeka.gaia.runner;

import com.github.mustachejava.DefaultMustacheFactory;
import io.codeka.gaia.config.security.StateApiSecurityConfig;
import io.codeka.gaia.modules.bo.TerraformModule;
import io.codeka.gaia.modules.bo.Variable;
import io.codeka.gaia.registries.RegistryOAuth2Provider;
Expand Down Expand Up @@ -35,7 +36,10 @@ class StackCommandBuilderTest {
@BeforeEach
void setup() {
var mustache = new DefaultMustacheFactory().compile("mustache/terraform.mustache");
stackCommandBuilder = new StackCommandBuilder(new Settings(), mustache, List.of(registryOAuth2Provider));

var stateApiSecurityProperties = new StateApiSecurityConfig.StateApiSecurityProperties("gaia-backend", "password");

stackCommandBuilder = new StackCommandBuilder(new Settings(), mustache, List.of(registryOAuth2Provider), stateApiSecurityProperties);
}

@Test
Expand Down
Loading

0 comments on commit 9f0054b

Please sign in to comment.