Skip to content

Commit

Permalink
🔒 : scope credentials to users
Browse files Browse the repository at this point in the history
  • Loading branch information
juwit committed Jul 12, 2020
1 parent ddcb7ce commit c787392
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 15 deletions.
3 changes: 3 additions & 0 deletions src/main/java/io/gaia_app/config/MongoConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;

@Configuration
@EnableMongoRepositories(basePackages = "io.gaia_app")
@EnableMongoAuditing
public class MongoConfig {

@Autowired
Expand All @@ -15,3 +17,4 @@ void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
}

}

18 changes: 18 additions & 0 deletions src/main/java/io/gaia_app/config/SpringSecurityUserAuditorAware.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.gaia_app.config

import io.gaia_app.teams.User
import io.gaia_app.teams.repository.UserRepository
import org.springframework.data.domain.AuditorAware
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Component
import java.util.*

@Component
class SpringSecurityUserAuditorAware(val userRepository: UserRepository):AuditorAware<User> {

override fun getCurrentAuditor(): Optional<User> {
val authentication = SecurityContextHolder.getContext().authentication
return userRepository.findById(authentication.name)
}

}
11 changes: 11 additions & 0 deletions src/main/java/io/gaia_app/credentials/Credentials.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import org.springframework.data.annotation.Id

import com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id.NAME
import io.gaia_app.teams.User
import org.springframework.data.annotation.CreatedBy
import org.springframework.data.mongodb.core.mapping.DBRef
import org.springframework.data.mongodb.core.mapping.Document


@JsonTypeInfo(use = NAME, include = PROPERTY, property = "provider")
Expand All @@ -23,20 +27,27 @@ abstract class Credentials {

lateinit var name: String

@CreatedBy
@DBRef
lateinit var createdBy: User

abstract fun toEnv(): List<String>
abstract fun provider(): String
}

@Document
data class AWSCredentials(val accessKey:String, val secretKey:String):Credentials() {
override fun toEnv() = listOf("AWS_ACCESS_KEY_ID=$accessKey", "AWS_SECRET_ACCESS_KEY=$secretKey")
override fun provider() = "aws"
}

@Document
data class GoogleCredentials(val serviceAccountJSONContents:String):Credentials() {
override fun toEnv() = listOf("GOOGLE_CREDENTIALS=$serviceAccountJSONContents")
override fun provider() = "google"
}

@Document
data class AzureRMCredentials(val clientId:String, val clientSecret:String):Credentials() {
override fun toEnv() = listOf("ARM_CLIENT_ID=$clientId", "ARM_CLIENT_SECRET=$clientSecret")
override fun provider() = "azurerm"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package io.gaia_app.credentials

import io.gaia_app.teams.User
import org.springframework.data.mongodb.repository.MongoRepository
import java.util.*

interface CredentialsRepository: MongoRepository<Credentials, String>
interface CredentialsRepository: MongoRepository<Credentials, String> {

fun findAllByCreatedBy(user: User): List<Credentials>

fun findByIdAndCreatedBy(id: String, createdBy: User): Optional<Credentials>
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.gaia_app.credentials;

import io.gaia_app.teams.User;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@RestController
@RequestMapping("/api/credentials")
@Secured({"ROLE_USER","ROLE_ADMIN"})
@Secured({"ROLE_USER", "ROLE_ADMIN"})
public class CredentialsRestController {

private CredentialsRepository credentialsRepository;
Expand All @@ -17,27 +20,36 @@ public CredentialsRestController(CredentialsRepository credentialsRepository) {
}

@GetMapping
public List<Credentials> getAllCredentials(){
return this.credentialsRepository.findAll();
public List<Credentials> getAllCredentials(User user) {
return this.credentialsRepository.findAllByCreatedBy(user);
}

@GetMapping("/{id}")
public Credentials getCredentials(@PathVariable String id){
return this.credentialsRepository.findById(id).orElse(null);
public Credentials getCredentials(@PathVariable String id, User user) {
return this.credentialsRepository.findByIdAndCreatedBy(id, user)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.FORBIDDEN));
}

@PostMapping
public Credentials createCredentials(@RequestBody Credentials credentials){
public Credentials createCredentials(@RequestBody Credentials credentials) {
return this.credentialsRepository.save(credentials);
}

@PutMapping("/{id}")
public Credentials updateCredentials(@RequestBody Credentials credentials, @PathVariable String id){
public Credentials updateCredentials(@RequestBody Credentials credentials, @PathVariable String id, User user) {
// checking if we have the rights on this credentials
if (this.credentialsRepository.findByIdAndCreatedBy(id, user).isEmpty()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
return this.credentialsRepository.save(credentials);
}

@DeleteMapping("/{id}")
public void deleteCredentials(@PathVariable String id){
public void deleteCredentials(@PathVariable String id, User user) {
// checking if we have the rights on this credentials
if (this.credentialsRepository.findByIdAndCreatedBy(id, user).isEmpty()) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
}
this.credentialsRepository.deleteById(id);
}
}
133 changes: 133 additions & 0 deletions src/test/java/io/gaia_app/credentials/CredentialsRestControllerIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package io.gaia_app.credentials;

import io.gaia_app.test.SharedMongoContainerTest;
import org.junit.jupiter.api.BeforeEach;
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.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@Testcontainers
@AutoConfigureMockMvc
class CredentialsRestControllerIT extends SharedMongoContainerTest {

@Autowired
private MockMvc mockMvc;

@BeforeEach
void setup() {
mongo.emptyDatabase();
mongo.runScript("src/test/resources/db/00_team.js");
mongo.runScript("src/test/resources/db/10_user.js");
mongo.runScript("src/test/resources/db/70_credentials.js");
}

@Test
@WithMockUser("Darth Vader")
void users_shouldBeAbleToViewTheirOwnCredentials_forListAccess() throws Exception {
mockMvc.perform(get("/api/credentials"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].provider", is("aws")))
.andExpect(jsonPath("$[0].accessKey", is("DEATH_STAR_KEY")))
.andExpect(jsonPath("$[0].secretKey", is("DEATH_STAR_SECRET")))
.andExpect(jsonPath("$[0].createdBy.username", is("Darth Vader")));
}

@Test
@WithMockUser("Darth Vader")
void users_shouldBeAbleToViewTheirOwnCredentials_forSingleAccess() throws Exception {
mockMvc.perform(get("/api/credentials/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("provider", is("aws")))
.andExpect(jsonPath("name", is("Holocron")))
.andExpect(jsonPath("accessKey", is("DEATH_STAR_KEY")))
.andExpect(jsonPath("secretKey", is("DEATH_STAR_SECRET")))
.andExpect(jsonPath("createdBy.username", is("Darth Vader")));
}

@Test
@WithMockUser("Luke Skywalker")
void users_shouldNotBeAbleToView_othersCredentials_forListAccess() throws Exception {
// Luke cannot see Vader's credentials
mockMvc.perform(get("/api/credentials"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(0)));
}

@Test
@WithMockUser("Luke Skywalker")
void users_shouldNotBeAbleToView_othersCredentials_forSingleAccess() throws Exception {
mockMvc.perform(get("/api/credentials/1"))
.andExpect(status().isForbidden());
}

@Test
@WithMockUser("Darth Vader")
void users_shouldBeAbleToUpdate_theirCredentials() throws Exception {
// Luke cannot see Vader's credentials
mockMvc.perform(
put("/api/credentials/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"_id\": \"1\",\n" +
" \"provider\": \"aws\",\n" +
" \"name\": \"Holocron-Updated\",\n" +
" \"accessKey\": \"DEATH_STAR_KEY\",\n" +
" \"secretKey\": \"DEATH_STAR_SECRET\",\n" +
" \"createdBy\": {\"username\": \"Darth Vader\"},\n" +
" \"_class\": \"io.gaia_app.credentials.AWSCredentials\"\n" +
" }"))
.andExpect(status().isOk())
.andExpect(jsonPath("name", is("Holocron-Updated")));
}

@Test
@WithMockUser("Luke Skywalker")
void users_shouldNotBeAbleToUpdate_othersCredentials() throws Exception {
mockMvc.perform(
put("/api/credentials/1")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("{\n" +
" \"_id\": \"1\",\n" +
" \"provider\": \"aws\",\n" +
" \"name\": \"Holocron-Updated\",\n" +
" \"accessKey\": \"DEATH_STAR_KEY\",\n" +
" \"secretKey\": \"DEATH_STAR_SECRET\",\n" +
" \"createdBy\": {\"username\": \"Darth Vader\"},\n" +
" \"_class\": \"io.gaia_app.credentials.AWSCredentials\"\n" +
" }"))
.andExpect(status().isForbidden());
}

@Test
@WithMockUser("Darth Vader")
void users_shouldBeAbleToDelete_theirCredentials() throws Exception {
// Luke cannot see Vader's credentials
mockMvc.perform(delete("/api/credentials/1").with(csrf()))
.andExpect(status().isOk());
}

@Test
@WithMockUser("Luke Skywalker")
void users_shouldNotBeAbleToDelete_othersCredentials() throws Exception {
// Luke cannot see Vader's credentials
mockMvc.perform(delete("/api/credentials/1").with(csrf()))
.andExpect(status().isForbidden());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ void users_shouldBeAccessible_forAdminUser() {
void users_shouldBeExposed_atSpecificUrl() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(4)))
.andExpect(jsonPath("$..username", contains("admin", "Mary J", "Darth Vader", "selmak")))
.andExpect(jsonPath("$..admin", contains(true, false, false, false)))
.andExpect(jsonPath("$", hasSize(5)))
.andExpect(jsonPath("$..username", contains("admin", "Mary J", "Darth Vader", "Luke Skywalker", "selmak")))
.andExpect(jsonPath("$..admin", contains(true, false, false, false, false)))
.andExpect(jsonPath("$..team.id", contains("Ze Team", "Not Ze Team")));
}

Expand Down Expand Up @@ -115,9 +115,9 @@ void users_canBeChangedOfTeam() throws Exception {
void users_shouldNotLeakTheirOAuth2Credentials() throws Exception {
mockMvc.perform(get("/api/users"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[3].username", is("selmak")))
.andExpect(jsonPath("$[3].oauth2User.provider", is("github")))
.andExpect(jsonPath("$[3].oauth2User.token").doesNotExist());
.andExpect(jsonPath("$[4].username", is("selmak")))
.andExpect(jsonPath("$[4].oauth2User.provider", is("github")))
.andExpect(jsonPath("$[4].oauth2User.token").doesNotExist());
}

}
3 changes: 3 additions & 0 deletions src/test/resources/db/10_user.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ gaia.user.insert([
{
"_id": "Darth Vader"
},
{
"_id": "Luke Skywalker"
},
{
"_id": "selmak",
"oAuth2User": {
Expand Down
13 changes: 13 additions & 0 deletions src/test/resources/db/70_credentials.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
gaia = db.getSiblingDB('gaia');
gaia.credentials.drop();
gaia.credentials.insert([
{
"_id": "1",
"provider": "aws",
"name": "Holocron",
"accessKey": "DEATH_STAR_KEY",
"secretKey": "DEATH_STAR_SECRET",
"createdBy": {"$ref": "user", "$id": "Darth Vader"},
"_class": "io.gaia_app.credentials.AWSCredentials"
}
]);

0 comments on commit c787392

Please sign in to comment.