Verified Commit 2d4da1cc authored by João Capucho's avatar João Capucho
Browse files

Seal secrets based on characteristic value type

parent dcb3f3f0
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -31,7 +31,7 @@

    <properties>
        <java.version>17</java.version>
        <org.etsi.osl.model.tmf.version>1.2.0</org.etsi.osl.model.tmf.version>
        <org.etsi.osl.model.tmf.version>1.3.0-alpha</org.etsi.osl.model.tmf.version>
    </properties>

    <repositories>
+23 −62
Original line number Diff line number Diff line
package org.etsi.osl.secretcontroller;

import org.etsi.osl.secretcontroller.config.RulesConfig;
import org.etsi.osl.secretcontroller.rules.CharacteristicRule;
import org.etsi.osl.secretcontroller.rules.ResourceRule;
import org.etsi.osl.secretcontroller.rules.RuleEvaluatorService;
import org.etsi.osl.tmf.common.model.EValueType;
import org.etsi.osl.tmf.ri639.model.Characteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tools.jackson.core.exc.StreamReadException;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

@Service
@@ -33,80 +29,45 @@ public class ResourceRulesService {
        this.mapper = mapper;
    }

    public Optional<ResourceRulesApplier> getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) {
        final var cat = resource.getCategory();
        final var rule = this.rulesConfig.getResourceRules().get(cat);
    public ResourceRulesApplier getResourceRules(org.etsi.osl.tmf.ri639.model.Resource resource) {
        ResourceRule rule = null;

        if (rule == null)
            return Optional.empty();
        final var cat = resource.getCategory();
        if (cat != null)
            rule = this.rulesConfig.getResourceRules().get(cat);

        return Optional.of(new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource, rule));
        return new ResourceRulesApplier(this.mapper, this.ruleEvaluatorService, resource,
                Optional.ofNullable(rule));
    }

    public static class ResourceRulesApplier {
        private final ResourceRule rule;
        private final Optional<ResourceRule> rule;
        private final RuleEvaluatorService ruleEvaluatorService;

        private final Map<String, CharacteristicRule> extraRules = new HashMap<>();

        public ResourceRulesApplier(ObjectMapper mapper,
                                    RuleEvaluatorService ruleEvaluatorService,
                                    org.etsi.osl.tmf.ri639.model.Resource resource,
                                    ResourceRule rule) {
                                    Optional<ResourceRule> rule) {
            this.rule = rule;
            this.ruleEvaluatorService = ruleEvaluatorService;

            if (!rule.isKubernetesSecret())
                return;

            // Kubernetes secret extra handling
            final var rawSecret = resource.getResourceCharacteristicByName("json").getValue().getValue();
            final JsonNode json;
            try {
                json = mapper.readTree(rawSecret);
            } catch (StreamReadException e) {
                // If reading fails the secret most likely has already been erased
                log.debug(e.getMessage());
                return;
            }

            final var secretData = json.get("data");
            // The secret may have already been erased
            if (secretData == null || !secretData.isObject())
                return;

            final var replaceRule = new CharacteristicRule();
            replaceRule.setAction(SecretApplyResult.Action.Replace);
            replaceRule.setBase64Decode(true);

            // Convert all characteristics with secret data into secrets
            for (final var field : secretData.propertyNames())
                this.extraRules.put(field, replaceRule);

            final var eraseRule = new CharacteristicRule();
            eraseRule.setAction(SecretApplyResult.Action.Erase);

            // All these fields contain the secret data in one way or another and must be erased
            this.extraRules.put("data", eraseRule);
            this.extraRules.put("json", eraseRule);
            this.extraRules.put("metadata", eraseRule);
        }

        public SecretApplyResult saveCharacteristic(Characteristic characteristic) {
            final var name = characteristic.getName();
            final var valueType = characteristic.getValueType();
            final var value = characteristic.getValue().getValue();

            final var extraRule = this.extraRules.get(name);
            if (extraRule != null)
                return this.ruleEvaluatorService.evaluateRule(characteristic.getValue().getValue(), extraRule);

            for (final var cRule : this.rule.getCharacteristics()) {
            if (this.rule.isPresent()) {
                for (final var cRule : this.rule.get().getCharacteristics()) {
                    final var m = cRule.getMatch().matcher(name);
                    if (!m.matches())
                        continue;

                final var value = characteristic.getValue().getValue();
                    return this.ruleEvaluatorService.evaluateRule(value, cRule);
                }
            } else if (valueType != null && valueType.equals(EValueType.SECRET.getValue())) {
                return SecretApplyResult.replace(value);
            }

            return SecretApplyResult.ignore();
        }
+18 −16
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ package org.etsi.osl.secretcontroller;
import org.etsi.osl.secretcontroller.config.RulesConfig;
import org.etsi.osl.secretcontroller.rules.RuleEvaluatorService;
import org.etsi.osl.secretcontroller.rules.ServiceRule;
import org.etsi.osl.tmf.common.model.EValueType;
import org.etsi.osl.tmf.common.model.service.Characteristic;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -20,36 +21,37 @@ public class ServiceRulesService {
        this.ruleEvaluatorService = ruleEvaluatorService;
    }

    public Optional<ServiceRulesApplier> getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) {
    public ServiceRulesApplier getServiceRules(org.etsi.osl.tmf.sim638.model.Service svc) {
        final var name = svc.getName();
        final var rule = this.rulesConfig.getServiceRules().get(name);

        if (rule == null)
            return Optional.empty();

        return Optional.of(new ServiceRulesApplier(this.ruleEvaluatorService, rule));
        return new ServiceRulesApplier(this.ruleEvaluatorService, Optional.ofNullable(rule));
    }

    public static class ServiceRulesApplier {
        private final ServiceRule rule;
        private final Optional<ServiceRule> rule;
        private final RuleEvaluatorService ruleEvaluatorService;

        public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, ServiceRule rule) {
        public ServiceRulesApplier(RuleEvaluatorService ruleEvaluatorService, Optional<ServiceRule> rule) {
            this.rule = rule;
            this.ruleEvaluatorService = ruleEvaluatorService;
        }

        public SecretApplyResult saveCharacteristic(Characteristic characteristic) {
            final var name = characteristic.getName();
            final var valueType = characteristic.getValueType();
            final var value = characteristic.getValue().getValue();

            for (final var cRule : this.rule.characteristics()) {
            if (this.rule.isPresent()) {
                for (final var cRule : this.rule.get().characteristics()) {
                    final var m = cRule.getMatch().matcher(name);
                    if (!m.matches())
                        continue;

                var value = characteristic.getValue().getValue();
                    return this.ruleEvaluatorService.evaluateRule(value, cRule);
                }
            } else if (valueType != null && valueType.equals(EValueType.SECRET.getValue())) {
                return SecretApplyResult.replace(value);
            }

            return SecretApplyResult.ignore();
        }
+26 −13
Original line number Diff line number Diff line
@@ -8,10 +8,10 @@ import org.springframework.vault.core.VaultKeyValueOperations;
import org.springframework.vault.core.VaultKeyValueOperationsSupport;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.web.util.UriUtils;
import tools.jackson.databind.ObjectMapper;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Optional;

@Service
public class VaultService {
@@ -29,31 +29,44 @@ public class VaultService {
        if (vaultUri.endsWith("/"))
            vaultUri = vaultUri.substring(0, vaultUri.length() - 1);

        this.vaultBaseSecretUri = String.format("%s/v1/%s", vaultUri, kvPath);
        this.vaultBaseSecretUri = String.format("%s/v1/%s/data/", vaultUri, kvPath);
    }

    public final String getVaultKey(String namespace, String key) {
        return String.format("%s::%s", namespace, key);
    public String getVaultKey(String namespace, String key) {
        return String.format("%s/%s", namespace, key);
    }

    public final String getVaultUri(String secretKey) {
        return String.format(
                "%s/data/%s",
                vaultBaseSecretUri,
                UriUtils.encodePathSegment(secretKey, StandardCharsets.US_ASCII)
        );
    public String getVaultUri(String secretKey) {
        return vaultBaseSecretUri + UriUtils.encodePath(secretKey, StandardCharsets.US_ASCII);
    }

    public boolean isAlreadySecret(String value) {
        return value.startsWith(this.vaultBaseSecretUri);
    }

    public Optional<String> extractVaultKeyFromUri(String secretUri) {
        if (!secretUri.startsWith(this.vaultBaseSecretUri))
            return Optional.empty();
        return Optional.of(secretUri.substring(this.vaultBaseSecretUri.length()));
    }

    public Optional<Map<String, ?>> getSecret(String key) {
        final var secret = this.vaultKV.get(key);
        if (secret == null)
            return Optional.empty();

        final var data = secret.getData();
        if (data == null)
            return Optional.empty();

        return Optional.of(data);
    }

    public void upsertSecret(String key, Map<String, ?> data) {
        final var secret = Map.of("data", data);
        try {
            this.vaultKV.patch(key, secret);
            this.vaultKV.patch(key, data);
        } catch (SecretNotFoundException e) {
            this.vaultKV.put(key, secret);
            this.vaultKV.put(key, data);
        }
    }

+67 −0
Original line number Diff line number Diff line
package org.etsi.osl.secretcontroller.listeners;

import org.etsi.osl.secretcontroller.VaultService;
import org.etsi.osl.secretcontroller.listeners.commands.UnsealCharacteristicCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.jms.core.JmsClient;
import org.springframework.jms.support.JmsHeaders;
import org.springframework.jms.support.JmsMessageHeaderAccessor;
import org.springframework.stereotype.Component;
import tools.jackson.databind.ObjectMapper;

import java.util.Map;

@Component
public class ControllerCommandsListener {
    private static final Logger log = LoggerFactory.getLogger(ControllerCommandsListener.class);
    private final VaultService vaultService;
    private final JmsClient jmsClient;
    private final ObjectMapper objectMapper;

    @Autowired
    public ControllerCommandsListener(VaultService vaultService, JmsClient jmsClient, ObjectMapper objectMapper) {
        this.vaultService = vaultService;
        this.jmsClient = jmsClient;
        this.objectMapper = objectMapper;
    }

    @JmsListener(destination = "${topics.SECRETS_CHARACTERISTIC_UNSEAL}")
    public void processUnsealCommand(String content, JmsMessageHeaderAccessor jmsHeaderAccessor) {
        final var destination = jmsHeaderAccessor.getReplyTo();
        if (destination == null) {
            log.warn("Unseal request: no reply destination");
            return;
        }

        final var cmd = this.objectMapper.readValue(content, UnsealCharacteristicCommand.class);

        final var vaultKeyOpt = this.vaultService.extractVaultKeyFromUri(cmd.secretURI());
        if (vaultKeyOpt.isEmpty()) {
            // Ignore this command as the URI isn't for this controller
            log.debug("Unseal request: secret URI does not belong to this controller");
            return;
        }
        final var vaultKey = vaultKeyOpt.get();

        Object result = null;

        final var secretData = this.vaultService.getSecret(vaultKey);
        if (secretData.isPresent()) {
            final var characteristicValue = secretData.get().get(cmd.characteristic());
            if (characteristicValue != null) {
                result = characteristicValue;
            } else {
                log.debug("Unseal request: Characteristic not found");
            }
        } else {
            log.debug("Unseal request: Secret not found");
        }

        this.jmsClient.destination(destination)
                .send(this.objectMapper.writeValueAsString(result),
                        Map.of(JmsHeaders.CORRELATION_ID, jmsHeaderAccessor.getCorrelationId()));
    }
}
Loading