Skip to content

Commit

Permalink
feat(common/service)!: add authorization for using secrets
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Becker <[email protected]>
  • Loading branch information
sbckr committed Nov 18, 2024
1 parent d301b96 commit 2874b41
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 79 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2024 - for information on the respective copyright owner
* see the NOTICE file and/or the repository https://github.com/carbynestack/amphora.
*
* SPDX-License-Identifier: Apache-2.0
*/

package io.carbynestack.amphora.common.rest;

import lombok.NonNull;
import lombok.Value;

@Value
public class UseRequest {
@NonNull
String programId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2024 - for information on the respective copyright owner
* see the NOTICE file and/or the repository https://github.com/carbynestack/amphora.
*
* SPDX-License-Identifier: Apache-2.0
*/

package io.carbynestack.amphora.common.rest;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.ValueInstantiationException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class UseRequestTest {

@Test
void givenNoProgramIdProvided_whenParseJson_thenThrowException() {
assertThrows(ValueInstantiationException.class, () ->
new ObjectMapper().readValue("{}", UseRequest.class));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public class OpaService {
public static final String OWNER_TAG_KEY = "owner";

static final String READ_SECRET_ACTION_NAME = "read";
static final String USE_SECRET_ACTION_NAME = "use";
static final String DELETE_SECRET_ACTION_NAME = "delete";
static final String CREATE_TAG_ACTION_NAME = "tag/create";
static final String READ_TAG_ACTION_NAME = "tag/read";
Expand All @@ -46,6 +47,19 @@ public boolean canReadSecret(String subject, List<Tag> tags) throws CsOpaExcepti
return isAllowed(subject, READ_SECRET_ACTION_NAME, tags);
}

/**
* Check if the subject can use the secret described by the given tags evaluating the OPA policy package.
* The policy package is extracted from the tags if present. If not present, the default policy package is used.
*
* @param subject The subject attempting to use the secret.
* @param tags The tags describing the referenced secret.
* @return True if the subject can use the secret, false otherwise.
* @throws CsOpaException If an error occurred while evaluating the policy.
*/
public boolean canUseSecret(String subject, List<Tag> tags) throws CsOpaException {
return isAllowed(subject, USE_SECRET_ACTION_NAME, tags);
}

/**
* Check if the subject can delete the secret described by the given tags evaluating the OPA policy package.
* The policy package is extracted from the tags if present. If not present, the default policy package is used.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,20 @@ public Page<Metadata> getSecretList(List<TagFilter> tagFilters, Sort sort) {
* if no secret with the given id exists.
* @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved.
* @throws NotFoundException if no {@link SecretShare} with the given id exists
* @throws UnauthorizedException if the requesting program is not authorized to access the {@link SecretShare}
*/
@Transactional(readOnly = true)
public SecretShare getSecretShareAuthorized(UUID secretId) {
return secretEntityRepository
.findById(secretId.toString())
.map(this::getSecretShareForEntity)
.orElseThrow(
() ->
new NotFoundException(
String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)));
public SecretShare useSecretShare(UUID secretId, String programId) throws UnauthorizedException, CsOpaException {
SecretEntity secretEntity = secretEntityRepository
.findById(secretId.toString())
.orElseThrow(
() ->
new NotFoundException(
String.format(NO_SECRET_WITH_ID_EXISTS_EXCEPTION_MSG, secretId)));
if (!opaService.canUseSecret(programId, setToTagList(secretEntity.getTags()))) {
throw new UnauthorizedException("Requesting program is not authorized to access the secret");
}
return getSecretShareForEntity(secretEntity);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
import io.carbynestack.amphora.common.SecretShare;
import io.carbynestack.amphora.common.Tag;
import io.carbynestack.amphora.common.exceptions.AmphoraServiceException;
import io.carbynestack.amphora.common.rest.UseRequest;
import io.carbynestack.amphora.service.exceptions.AlreadyExistsException;
import io.carbynestack.amphora.service.exceptions.CsOpaException;
import io.carbynestack.amphora.service.exceptions.NotFoundException;
import io.carbynestack.amphora.service.exceptions.UnauthorizedException;
import io.carbynestack.amphora.service.persistence.metadata.StorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -26,6 +29,7 @@
import java.util.UUID;

import static io.carbynestack.amphora.common.rest.AmphoraRestApiEndpoints.*;
import static org.springframework.util.Assert.notNull;

@Slf4j
@RestController
Expand Down Expand Up @@ -65,9 +69,14 @@ public ResponseEntity<URI> uploadSecretShare(@RequestBody SecretShare secretShar
* @return {@link HttpStatus#OK} with the {@link SecretShare} if successful.
* @throws AmphoraServiceException if an {@link SecretShare} exists but could not be retrieved.
* @throws NotFoundException if no {@link SecretShare} with the given id exists
* @throws UnauthorizedException if the requesting Program is not authorized to access the {@link SecretShare}
* @throws CsOpaException if an error occurred while evaluating the OPA policy
*/
@GetMapping(path = "/{" + SECRET_ID_PARAMETER + "}")
public ResponseEntity<SecretShare> downloadSecretShare(@PathVariable UUID secretId) {
return new ResponseEntity<>(storageService.getSecretShareAuthorized(secretId), HttpStatus.OK);
public ResponseEntity<SecretShare> downloadSecretShare(@PathVariable UUID secretId,
@RequestBody UseRequest request) throws UnauthorizedException, CsOpaException {

notNull(request, "Request body must be valid");
return new ResponseEntity<>(storageService.useSecretShare(secretId, request.getProgramId()), HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@
import java.util.List;

import static io.carbynestack.amphora.service.opa.OpaService.READ_SECRET_ACTION_NAME;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static io.carbynestack.amphora.service.opa.OpaService.USE_SECRET_ACTION_NAME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
public class OpaServiceTest {
class OpaServiceTest {

private static final String POLICY_PACKAGE = "play";
private static final String DEFAULT_POLICY_PACKAGE = "default";
Expand Down Expand Up @@ -54,7 +55,7 @@ public void setUp() {
}

@Test
public void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException {
void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided() throws CsOpaException {
ArgumentCaptor<String> packageCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> actionCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> subjectCaptor = ArgumentCaptor.forClass(String.class);
Expand All @@ -68,7 +69,7 @@ public void givenPolicyDefinedInTag_whenIsAllowed_thenUsePolicyPackageProvided()
testTags.add(POLICY_TAG);
boolean result = service.isAllowed(SUBJECT, READ_SECRET_ACTION_NAME, testTags);

assertTrue("must be allowed", result);
assertTrue(result, "must be allowed");
String actualPackage = packageCaptor.getValue();
assertEquals(POLICY_TAG.getValue(), actualPackage);
String actualAction = actionCaptor.getValue();
Expand All @@ -90,7 +91,7 @@ public void givenNoPolicyDefinedInTag_whenIsAllowed_thenUseDefaultPolicyPackage(
OpaService service = new OpaService(opaClientMock);
boolean result = service.isAllowed(READ_SECRET_ACTION_NAME, SUBJECT, TAGS);

assertTrue("must be allowed", result);
assertTrue(result, "must be allowed");
String actualPackage = packageCaptor.getValue();
assertEquals(DEFAULT_POLICY_PACKAGE, actualPackage);
List<Tag> actualTags = tagsCaptor.getValue();
Expand All @@ -112,6 +113,21 @@ public void whenCanReadSecret_thenUseProperAction() throws CsOpaException {

}

@Test
public void whenCanUseSecret_thenUseProperAction() throws CsOpaException {
ArgumentCaptor<String> actionCaptor = ArgumentCaptor.forClass(String.class);
when(opaClientMock.isAllowed(
any(), actionCaptor.capture(), any(), any()))
.thenReturn(true);

OpaService service = new OpaService(opaClientMock);
service.canUseSecret(SUBJECT, TAGS);

String actualAction = actionCaptor.getValue();
assertEquals(USE_SECRET_ACTION_NAME, actualAction);

}

@Test
public void whenCanDeleteSecret_thenUseProperAction() throws CsOpaException {
ArgumentCaptor<String> actionCaptor = ArgumentCaptor.forClass(String.class);
Expand Down
Loading

0 comments on commit 2874b41

Please sign in to comment.