Skip to content

Commit

Permalink
feat(chart/service)!: add Authorization (#74)
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Becker <[email protected]>
  • Loading branch information
sbckr authored Dec 17, 2024
1 parent 4a77d30 commit 4aeac00
Show file tree
Hide file tree
Showing 35 changed files with 1,883 additions and 268 deletions.
2 changes: 1 addition & 1 deletion amphora-service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ DB_USER=user
DB_PASSWORD=secret
MINIO_ENDPOINT=http://localhost:9000
EOF
docker run --env-file amphora.conf iotspecs/amphora-service
docker run --env-file amphora.conf iotspecs/amphora-service
```
6 changes: 6 additions & 0 deletions amphora-service/charts/amphora/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ spec:
value: {{ .Values.amphora.springActiveProfiles }}
- name: CASTOR_SERVICE_URI
value: "{{ .Values.amphora.castor.serviceUri }}"
- name: AMPHORA_USER_ID_FIELD_NAME
value: "{{ .Values.auth.userIdFieldName }}"
- name: AMPHORA_OPA_DEFAULT_POLICY_PACKAGE
value: "{{ .Values.opa.defaultPolicyPackage }}"
- name: AMPHORA_OPA_URL
value: "{{ .Values.opa.endpoint }}"
- name: MAC_KEY
value: "{{ .Values.spdz.macKey }}"
- name: SPDZ_PRIME
Expand Down
7 changes: 7 additions & 0 deletions amphora-service/charts/amphora/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,10 @@ spdz:
prime: ""
r: ""
rInv: ""

auth:
userIdFieldName: "sub"

opa:
defaultPolicyPackage: "carbynestack.def"
endpoint: "http://opa.default.svc.cluster.local:8081/"
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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.service.config;

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "carbynestack.auth")
@Component
@Data
@Accessors(chain = true)
public class AuthProperties {
private String userIdFieldName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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.service.config;

import io.carbynestack.amphora.service.opa.JwtReader;
import io.carbynestack.amphora.service.opa.OpaClient;
import java.net.URI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpaConfig {

@Bean
JwtReader jwtReader(AuthProperties authProperties) {
return new JwtReader(authProperties.getUserIdFieldName());
}

@Bean
OpaClient opaClient(OpaProperties opaProperties) {
return OpaClient.builder()
.opaServiceUri(URI.create(opaProperties.getEndpoint()))
.defaultPolicyPackage(opaProperties.getDefaultPolicyPackage())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.service.config;

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "carbynestack.opa")
@Component
@Data
@Accessors(chain = true)
public class OpaProperties {

private String endpoint;
private String defaultPolicyPackage;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.service.exceptions;

public class CsOpaException extends Exception {
public CsOpaException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* 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.service.exceptions;

public class UnauthorizedException extends Exception {
public UnauthorizedException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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.service.opa;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.carbynestack.amphora.service.exceptions.UnauthorizedException;
import io.vavr.control.Option;
import java.util.Base64;

public class JwtReader {
static ObjectMapper mapper = new ObjectMapper();
private final String userIdField;

public JwtReader(String userIdField) {
this.userIdField = userIdField;
}

public String extractUserIdFromAuthHeader(String header) throws UnauthorizedException {
return extractFieldFromAuthHeader(header, userIdField);
}

private static String extractFieldFromAuthHeader(String header, String field)
throws UnauthorizedException {
return tokenFromHeader(header)
.flatMap(JwtReader::dataNodeFromToken)
.flatMap(node -> fieldFromNode(node, field))
.getOrElseThrow(() -> new UnauthorizedException("No token provided"));
}

private static Option<JsonNode> dataNodeFromToken(String token) {
String[] parts = token.split("\\.");
if (parts.length != 3) {
return Option.none();
}
try {
String jwt = new String(Base64.getDecoder().decode(parts[1]));
return Option.of(mapper.reader().readTree(jwt));
} catch (JsonProcessingException e) {
return Option.none();
}
}

private static Option<String> tokenFromHeader(String header) {
if (header != null && header.startsWith("Bearer ")) {
return Option.of(header.substring(7));
}
return Option.none();
}

private static Option<String> fieldFromNode(JsonNode node, String fieldName) {
JsonNode field = node;
try {
for (String f : fieldName.split("\\.")) {
field = field.get(f);
}
return Option.of(field.asText());
} catch (NullPointerException e) {
return Option.none();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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.service.opa;

import io.carbynestack.amphora.common.Tag;
import io.carbynestack.httpclient.CsHttpClient;
import io.carbynestack.httpclient.CsHttpClientException;
import java.net.URI;
import java.util.List;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class OpaClient {
private final CsHttpClient<String> csHttpClient;
private final URI opaServiceUri;
private final String defaultPolicyPackage;

@Builder
public OpaClient(URI opaServiceUri, String defaultPolicyPackage) {
this(CsHttpClient.createDefault(), opaServiceUri, defaultPolicyPackage);
}

OpaClient(CsHttpClient<String> httpClient, URI opaServiceUri, String defaultPolicyPackage) {
this.csHttpClient = httpClient;
this.opaServiceUri = opaServiceUri;
this.defaultPolicyPackage = defaultPolicyPackage;
}

/**
* Evaluate the OPA policy package with the given action, subject and tags.
*
* @param policyPackage The OPA policy package to evaluate.
* @param action The action to evaluate.
* @param subject The subject attempting to perform the action.
* @param tags The tags describing the accessed object.
* @return True if the subject can perform the action, false otherwise (or if an error occurred).
*/
public boolean isAllowed(String policyPackage, String action, String subject, List<Tag> tags) {
OpaRequestBody body = OpaRequestBody.builder().subject(subject).tags(tags).build();
try {
return csHttpClient
.postForObject(
opaServiceUri.resolve(
String.format("/v1/data/%s/%s", policyPackage.replace(".", "/"), action)),
new OpaRequest(body),
OpaResult.class)
.isAllowed();
} catch (CsHttpClientException e) {
log.error("Error occurred while evaluating OPA policy package", e);
}
return false;
}

public OpaClientRequest newRequest() {
return new OpaClientRequest(this, defaultPolicyPackage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.service.opa;

import io.carbynestack.amphora.common.Tag;
import io.carbynestack.amphora.service.exceptions.CsOpaException;
import java.util.List;
import java.util.Objects;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.logging.log4j.util.Strings;

@Setter()
@Accessors(chain = true, fluent = true)
public class OpaClientRequest {
private String withPolicyPackage;
private String withSubject;
private List<Tag> withTags;
private String withAction;

private final OpaClient opaClient;

public OpaClientRequest(OpaClient opaClient, String defaultPolicyPackage) {
this.withPolicyPackage = defaultPolicyPackage;
this.opaClient = opaClient;
}

public boolean evaluate() throws CsOpaException {
if (Strings.isEmpty(withSubject)) {
throw new CsOpaException("Subject is required to evaluate the policy");
}
if (Strings.isEmpty(withAction)) {
throw new CsOpaException("Action is required to evaluate the policy");
}
withTags.removeIf(Objects::isNull);
return opaClient.isAllowed(withPolicyPackage, withAction, withSubject, withTags);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.service.opa;

import lombok.Getter;

@Getter
class OpaRequest {
OpaRequestBody input;

public OpaRequest(OpaRequestBody input) {
this.input = input;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.service.opa;

import io.carbynestack.amphora.common.Tag;
import java.util.List;
import lombok.Builder;
import lombok.Value;

@Value
public class OpaRequestBody {
String subject;
List<Tag> tags;

@Builder
public OpaRequestBody(String subject, List<Tag> tags) {
this.subject = subject;
this.tags = tags;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.service.opa;

import lombok.Setter;

public class OpaResult {
@Setter private boolean result;

boolean isAllowed() {
return result;
}
}
Loading

0 comments on commit 4aeac00

Please sign in to comment.