From 6f5b9864ed73e3317d96050b3abc19fdd5151f6a Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Tue, 18 Jul 2023 09:35:35 +0100 Subject: [PATCH 01/11] docker: add DEBUG_PORT make configuration Allow forwarding a port of the debug container stated with `make debug` by setting the DEBUG_PORT environment variable to the value of the port to be forwarded. Signed-off-by: Sergei Trofimov --- deployments/docker/Makefile | 6 +++++- deployments/docker/README.md | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/deployments/docker/Makefile b/deployments/docker/Makefile index b6b59610..c551ca5d 100644 --- a/deployments/docker/Makefile +++ b/deployments/docker/Makefile @@ -27,6 +27,10 @@ vts_FLAGS := -v $(STORES_VOLUME):/opt/veraison/stores provisioning_FLAGS := -p $(PROVISIONING_PORT):$(PROVISIONING_PORT) verification_FLAGS := -p $(VERIFICATION_PORT):$(VERIFICATION_PORT) +ifneq ($(DEBUG_PORT),) +DEBUG_PORT_FLAG := -p $(DEBUG_PORT):$(DEBUG_PORT) +endif + .PHONY: all all: builder build deploy deployment prune @@ -71,7 +75,7 @@ debug: .built/builder .built/network --network $(VERAISON_NETWORK) \ -v $(STORES_VOLUME):/veraison/stores -v $(LOGS_VOLUME):/veraison/logs \ -v $(THIS_DIR)../..:/veraison/build -v $(DEPLOY_DEST):/veraison/deploy \ - $(DEBUG_FLAGS) --hostname $(DEBUG_HOST)\ + $(DEBUG_FLAGS) $(DEBUG_PORT_FLAG) --hostname $(DEBUG_HOST)\ -i -t --user $(DEBUG_USER) --entrypoint /bin/bash \ veraison/builder diff --git a/deployments/docker/README.md b/deployments/docker/README.md index b8b5f8c6..b888aae1 100644 --- a/deployments/docker/README.md +++ b/deployments/docker/README.md @@ -82,6 +82,10 @@ make targets: want to substitute the debug container in place of one of the service containers. +`DEBUG_PORT`: if set with a port number, that port on the debug container will +be forwarded to the host (debug container is run with `-p +$(DEBUG_PORT):$(DEBUG_PORT)`). + `DOCKER_BUILD_FLAGS`: additional flags to be passed to Docker when building various images. This is passed to all image build invocations, so should only be used for globally-applicable flags such as `--no-cache`. From 98952e8ed663057bfde1390c8660010b228192b5 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Wed, 5 Jul 2023 10:44:55 +0100 Subject: [PATCH 02/11] integ-test: clear stores between tests Clear Veraison stores between tests, ensuring each test starts from a clean slate and is thus guaranteed to be independent of the preceding tests. Signed-off-by: Sergei Trofimov --- integration-tests/Makefile | 3 ++- integration-tests/docker/Dockerfile | 4 +++- integration-tests/docker/bashrc | 8 ++++++++ integration-tests/utils/conftest.py | 3 ++- integration-tests/utils/hooks.py | 1 + integration-tests/utils/util.py | 7 +++++++ 6 files changed, 23 insertions(+), 3 deletions(-) diff --git a/integration-tests/Makefile b/integration-tests/Makefile index aba63116..7a45d5ad 100644 --- a/integration-tests/Makefile +++ b/integration-tests/Makefile @@ -23,7 +23,8 @@ $(DEPLOYMENT_SRC_DIR).built/%: CONTAINER_FLAGS := --env-file $(DEPLOYMENT_SRC_DIR)deployment.cfg --network veraison-net \ - -v $(THIS_DIR):/integration-tests + -v $(THIS_DIR):/integration-tests \ + -v $(STORES_VOLUME):/opt/veraison/stores DEPLOYMENT_DEPS := $(DEPLOYMENT_SRC_DIR).built/network \ $(DEPLOYMENT_SRC_DIR).built/vts-container \ diff --git a/integration-tests/docker/Dockerfile b/integration-tests/docker/Dockerfile index 0d133ed6..b17a8c7d 100644 --- a/integration-tests/docker/Dockerfile +++ b/integration-tests/docker/Dockerfile @@ -16,6 +16,7 @@ RUN apt-get update \ ruby \ jq \ vim \ + sqlite3 \ && apt-get clean \ && apt-get autoremove --assume-yes \ && rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* && \ @@ -32,7 +33,8 @@ RUN wget https://dl.step.sm/gh-release/cli/docs-cli-install/v0.23.1/step-cli_0.2 RUN userdel -f $(cat /etc/passwd | awk -F: "\$3 == ${TESTER_UID}" | cut -d: -f1); \ groupdel -f $(cat /etc/group | awk -F: "\$3 == ${TESTER_GID}" | cut -d: -f1); \ groupadd -g ${TESTER_GID} tavern && \ - useradd -m -u ${TESTER_UID} -g tavern \ + groupadd -g 616 veraison && \ + useradd -m -u ${TESTER_UID} -g tavern -G veraison \ -s /bin/bash tavern WORKDIR /integration-tests diff --git a/integration-tests/docker/bashrc b/integration-tests/docker/bashrc index 5487be58..bf73cd4f 100644 --- a/integration-tests/docker/bashrc +++ b/integration-tests/docker/bashrc @@ -7,7 +7,15 @@ export PS1='\e[0;32m\u@debug-container \e[0;34m\w\n\e[0;32m$\e[0m ' alias ll='ls -lh --color=auto' alias jwt='step crypto jwt' +_stores_dir=/opt/veraison/stores/vts + function inspect-result() { local file=$1 step crypto jwt inspect --insecure < $file } + +function clear_stores() { + sqlite3 $_stores_dir/en-store.sql 'delete from kvstore' + sqlite3 $_stores_dir/po-store.sql 'delete from kvstore' + sqlite3 $_stores_dir/ta-store.sql 'delete from kvstore' +} diff --git a/integration-tests/utils/conftest.py b/integration-tests/utils/conftest.py index e459a5a8..e0643f6e 100644 --- a/integration-tests/utils/conftest.py +++ b/integration-tests/utils/conftest.py @@ -2,7 +2,7 @@ import sys import hooks -from util import to_identifier +from util import to_identifier, clear_stores class TavernTest: @@ -23,6 +23,7 @@ def pytest_tavern_beta_before_every_test_run(test_dict, variables): setup(test, variables) def setup(test, variables): + clear_stores() test_id = to_identifier(test.name) handler = getattr(hooks, f'setup_{test_id}', None) if handler: diff --git a/integration-tests/utils/hooks.py b/integration-tests/utils/hooks.py index ab3c2fcb..962aef6a 100644 --- a/integration-tests/utils/hooks.py +++ b/integration-tests/utils/hooks.py @@ -1,6 +1,7 @@ import os from generators import * +from util import run_command def setup_end_to_end(test, variables): diff --git a/integration-tests/utils/util.py b/integration-tests/utils/util.py index 543f98b8..34694db3 100644 --- a/integration-tests/utils/util.py +++ b/integration-tests/utils/util.py @@ -41,3 +41,10 @@ def run_command(command: str, action: str) -> int: executable = command.split()[0] raise RuntimeError(f'Could not {action}; {executable} returned {proc.returncode}') + +def clear_stores(): + for prefix in ['en', 'po', 'ta']: + command = f"sqlite3 /opt/veraison/stores/vts/{prefix}-store.sql 'delete from kvstore'" + run_command(command, f'clear {prefix} store') + + From 6b53da82c96d3344018436933c3102ebb4a4650c Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Wed, 5 Jul 2023 10:20:22 +0100 Subject: [PATCH 03/11] integ-test: fix CoRIM mini template The template was missing the mandatory "profile" entry, resulting in failing provisioning. This was obscured by the previous test case that was successfully provisioning using the full template, when the provisioned stores were not cleared between tests. Signed-off-by: Sergei Trofimov --- integration-tests/data/endorsements/corim-psa-mini.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/integration-tests/data/endorsements/corim-psa-mini.json b/integration-tests/data/endorsements/corim-psa-mini.json index bc7e70ba..f0116feb 100644 --- a/integration-tests/data/endorsements/corim-psa-mini.json +++ b/integration-tests/data/endorsements/corim-psa-mini.json @@ -1,3 +1,6 @@ { - "corim-id": "5c57e8f4-46cd-421b-91c9-08cf93e13cfc" + "corim-id": "5c57e8f4-46cd-421b-91c9-08cf93e13cfc", + "profiles": [ + "http://arm.com/psa/iot/1" + ] } From 32b2849183051a59414965542259683e5b95078b Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Tue, 18 Jul 2023 15:16:19 +0100 Subject: [PATCH 04/11] capability: allow schemes in lieu of/addition to media-types - Make media-types entry inside well-known info optional. - Add optional attestation-schemes entry. Current services continue to report media types and omit schemes, reporting the same info as before. This change is intended to support the coming management service. Signed-off-by: Sergei Trofimov --- capability/well-known.go | 13 +++++++++++-- provisioning/api/handler.go | 2 +- provisioning/api/handler_test.go | 1 - verification/api/handler.go | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/capability/well-known.go b/capability/well-known.go index d9901e5f..413b5791 100644 --- a/capability/well-known.go +++ b/capability/well-known.go @@ -10,7 +10,8 @@ const ( type WellKnownInfo struct { PublicKey jwk.Key `json:"ear-verification-key,omitempty"` - MediaTypes []string `json:"media-types"` + MediaTypes []string `json:"media-types,omitempty"` + Schemes []string `json:"attestation-schemes,omitempty"` Version string `json:"version"` ServiceState string `json:"service-state"` ApiEndpoints map[string]string `json:"api-endpoints"` @@ -32,11 +33,19 @@ func ServiceStateToAPI(ss string) string { return t } -func NewWellKnownInfoObj(key jwk.Key, mediaTypes []string, version string, serviceState string, endpoints map[string]string) (*WellKnownInfo, error) { +func NewWellKnownInfoObj( + key jwk.Key, + mediaTypes []string, + schemes []string, + version string, + serviceState string, + endpoints map[string]string, +) (*WellKnownInfo, error) { // MUST be kept in sync with proto/state.proto obj := &WellKnownInfo{ PublicKey: key, MediaTypes: mediaTypes, + Schemes: schemes, Version: version, ServiceState: ServiceStateToAPI(serviceState), ApiEndpoints: endpoints, diff --git a/provisioning/api/handler.go b/provisioning/api/handler.go index 60706f70..2cc48cdb 100644 --- a/provisioning/api/handler.go +++ b/provisioning/api/handler.go @@ -205,7 +205,7 @@ func (o *Handler) GetWellKnownProvisioningInfo(c *gin.Context) { endpoints := getProvisioningEndpoints() // Get final object with well known information - obj, err := capability.NewWellKnownInfoObj(nil, mediaTypes, version, state, endpoints) + obj, err := capability.NewWellKnownInfoObj(nil, mediaTypes, nil, version, state, endpoints) if err != nil { ReportProblem(c, http.StatusInternalServerError, diff --git a/provisioning/api/handler_test.go b/provisioning/api/handler_test.go index 8753b298..4b18c58b 100644 --- a/provisioning/api/handler_test.go +++ b/provisioning/api/handler_test.go @@ -294,7 +294,6 @@ func TestHandler_GetWellKnownProvisioningInfo_GetRegisteredMediaTypes_empty(t *t expectedCode := http.StatusOK expectedType := capability.WellKnownMediaType expectedBody := capability.WellKnownInfo{ - MediaTypes: []string{}, Version: testGoodServiceState.ServerVersion, ServiceState: capability.ServiceStateToAPI(testGoodServiceState.Status.String()), ApiEndpoints: publicApiMap, diff --git a/verification/api/handler.go b/verification/api/handler.go index 295e7ea5..71c44236 100644 --- a/verification/api/handler.go +++ b/verification/api/handler.go @@ -562,7 +562,7 @@ func (o *Handler) GetWellKnownVerificationInfo(c *gin.Context) { endpoints := getVerificationEndpoints() // Get final object with well known information - obj, err := capability.NewWellKnownInfoObj(key, mediaTypes, version, state, endpoints) + obj, err := capability.NewWellKnownInfoObj(key, mediaTypes, nil, version, state, endpoints) if err != nil { ReportProblem(c, http.StatusInternalServerError, From 99299a2dc88d253cd76f8382d12bd59b4615f834 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Wed, 5 Jul 2023 14:31:44 +0100 Subject: [PATCH 05/11] log: add a way to get an io.Writer Add methods to create an io.Writer associated with a named logger. Signed-off-by: Sergei Trofimov --- log/log.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/log/log.go b/log/log.go index a0d98449..d366f369 100644 --- a/log/log.go +++ b/log/log.go @@ -5,6 +5,7 @@ package log import ( "bytes" "fmt" + "io" "sort" "strings" "text/template" @@ -283,6 +284,50 @@ func Named(name string) *zap.SugaredLogger { return logger.Named(name) } +// NamedWriter creates an io.Writer that utilizes a zap logger with the +// specified name at the specifed level. +func NamedWriter(name string, level zapcore.Level) io.Writer { + return WriterFromZap(Named(name), level) +} + +type logWriter struct { + write func(args ...interface{}) +} + +func (o logWriter) Write(p []byte) (int, error) { + o.write(strings.TrimSuffix(string(p), "\n")) + return len(p), nil +} + +const ( + DebugLevel = zap.DebugLevel + TraceLevel = zap.DebugLevel + InfoLevel = zap.InfoLevel + WarnLevel = zap.WarnLevel + ErrorLevel = zap.ErrorLevel +) + +// WriterFromZap returns an io.Writer utilzing the provided zap logger at the +// specified level. +func WriterFromZap(logger *zap.SugaredLogger, level zapcore.Level) io.Writer { + var writeFunc func(args ...interface{}) + + switch level { + case zapcore.DebugLevel: + writeFunc = logger.Debug + case zapcore.InfoLevel: + writeFunc = logger.Info + case zapcore.WarnLevel: + writeFunc = logger.Warn + case zapcore.ErrorLevel: + writeFunc = logger.Error + default: + panic(fmt.Sprintf("unexpected level name: %q", level)) + } + + return logWriter{writeFunc} +} + // Debug uses fmt.Sprint to construct and log a message. func Debug(args ...interface{}) { logger.Debug(args...) From a5d43a0c821097369db98fc8751a09fddc9fcc0f Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Fri, 23 Jun 2023 16:00:09 +0100 Subject: [PATCH 06/11] Consistent AppraisalPolicyID and scheme-specific policies "Policy" as defined by the RATS architecture[1], and, therefore, the ear.appraisal-policy-id entry n EAR[2], maps onto the combination of attestation scheme and policy in Veraison. This means that, when a policy is not used, the ear.appraisal-policy-id field should be set to reflect the attestation scheme. If a policy is used, the field should be set to reflect both, the scheme and the policy. Additionally, up to this point, the policy manager allowed only one active policy per tenant. Differentiation between schemes, if necessary, could be performed within the policy rules. This commit changes this so that the polices are now managed based on both, the tenant and the scheme. This means that policies for different schemes can be updated independently by the tenant. [1]: https://datatracker.ietf.org/doc/html/draft-ietf-rats-architecture-05 [2]: https://www.rfc-editor.org/rfc/internet-drafts/draft-fv-rats-ear-00.html Signed-off-by: Sergei Trofimov --- integration-tests/data/results/cca.good.json | 1 + .../data/results/enacttrust.good.json | 1 + .../data/results/psa.badcrypto.json | 1 + .../data/results/psa.badinstance.json | 1 + .../data/results/psa.badswcomp.json | 2 +- integration-tests/data/results/psa.good.json | 2 +- .../data/results/psa.noident.json | 1 + integration-tests/utils/checkers.py | 4 + policy/README.md | 81 ++++++++ policy/agent.go | 5 +- policy/agent_test.go | 12 +- policy/cmd/polcli/commands/add.go | 14 +- policy/cmd/polcli/commands/del.go | 7 +- policy/cmd/polcli/commands/get.go | 35 ++-- policy/cmd/polcli/commands/list.go | 4 +- policy/id.go | 42 ---- policy/policy.go | 58 +++++- policy/policy_key.go | 63 ++++++ policy/policy_key_test.go | 37 ++++ policy/store.go | 194 ++++++++++++++---- policy/store_test.go | 57 +++-- policy/test/inputs/psa-result.json | 1 + vts/appraisal/appraisal.go | 33 ++- vts/policymanager/policymanager.go | 33 +-- vts/policymanager/policymanager_test.go | 85 +++++--- vts/trustedservices/trustedservices_grpc.go | 1 + 26 files changed, 589 insertions(+), 186 deletions(-) delete mode 100644 policy/id.go create mode 100644 policy/policy_key.go create mode 100644 policy/policy_key_test.go diff --git a/integration-tests/data/results/cca.good.json b/integration-tests/data/results/cca.good.json index 53cc7440..59e7ef26 100644 --- a/integration-tests/data/results/cca.good.json +++ b/integration-tests/data/results/cca.good.json @@ -10,6 +10,7 @@ "sourced-data": 0, "storage-opaque": 2 }, + "ear.appraisal-policy-id": "policy:CCA_SSD_PLATFORM", "ear.veraison.annotated-evidence": { "cca-platform-challenge": "Bea1iETGoM0ZOCBpuv2w5JRmKjrc+P3hFHjpM5Ua8XkP9d5ceOPbESPaCiB6i2ZVbgoi8Z7mS9wviZU7azJVXw==", "cca-platform-config": "AQID", diff --git a/integration-tests/data/results/enacttrust.good.json b/integration-tests/data/results/enacttrust.good.json index 89188afb..09b6c2ec 100644 --- a/integration-tests/data/results/enacttrust.good.json +++ b/integration-tests/data/results/enacttrust.good.json @@ -10,6 +10,7 @@ "sourced-data": 0, "storage-opaque": 0 }, + "ear.appraisal-policy-id": "policy:TPM_ENACTTRUST", "ear.veraison.annotated-evidence": { "node-id": "7df7714e-aa04-4638-bcbf-434b1dd720f1", "firmware-version": 7, diff --git a/integration-tests/data/results/psa.badcrypto.json b/integration-tests/data/results/psa.badcrypto.json index e7fdbb4b..226e6e47 100644 --- a/integration-tests/data/results/psa.badcrypto.json +++ b/integration-tests/data/results/psa.badcrypto.json @@ -10,6 +10,7 @@ "sourced-data": 99, "storage-opaque": 99 }, + "ear.appraisal-policy-id": "policy:PSA_IOT", "ear.veraison.policy-claims": { "problem": "signature validation failed" } diff --git a/integration-tests/data/results/psa.badinstance.json b/integration-tests/data/results/psa.badinstance.json index a0821bc2..3b8c5f56 100644 --- a/integration-tests/data/results/psa.badinstance.json +++ b/integration-tests/data/results/psa.badinstance.json @@ -10,6 +10,7 @@ "storage-opaque": 99, "sourced-data": 99 }, + "ear.appraisal-policy-id": "policy:PSA_IOT", "ear.veraison.policy-claims": { "problem": "no trust anchor for evidence" } diff --git a/integration-tests/data/results/psa.badswcomp.json b/integration-tests/data/results/psa.badswcomp.json index d7ad42ce..eea443fa 100644 --- a/integration-tests/data/results/psa.badswcomp.json +++ b/integration-tests/data/results/psa.badswcomp.json @@ -10,7 +10,7 @@ "storage-opaque": 2, "sourced-data": 0 }, - "ear.appraisal-policy-id": "policy://PSA_OIT", + "ear.appraisal-policy-id": "policy:PSA_IOT", "ear.veraison.annotated-evidence": { "eat-profile": "http://arm.com/psa/2.0.0", "psa-client-id": 1, diff --git a/integration-tests/data/results/psa.good.json b/integration-tests/data/results/psa.good.json index 66375eb1..fda08db1 100644 --- a/integration-tests/data/results/psa.good.json +++ b/integration-tests/data/results/psa.good.json @@ -10,7 +10,7 @@ "storage-opaque": 2, "sourced-data": 0 }, - "ear.appraisal-policy-id": "policy://PSA_OIT", + "ear.appraisal-policy-id": "policy:PSA_IOT", "ear.veraison.annotated-evidence": { "eat-profile": "http://arm.com/psa/2.0.0", "psa-client-id": 1, diff --git a/integration-tests/data/results/psa.noident.json b/integration-tests/data/results/psa.noident.json index 1c45206a..d078de5a 100644 --- a/integration-tests/data/results/psa.noident.json +++ b/integration-tests/data/results/psa.noident.json @@ -10,6 +10,7 @@ "sourced-data": 99, "storage-opaque": 99 }, + "ear.appraisal-policy-id": "policy:PSA_IOT", "ear.veraison.policy-claims": { "problem": "could not establish identity from evidence" } diff --git a/integration-tests/utils/checkers.py b/integration-tests/utils/checkers.py index 18fbd17b..3adca72a 100644 --- a/integration-tests/utils/checkers.py +++ b/integration-tests/utils/checkers.py @@ -32,6 +32,10 @@ def compare_to_expected_result(response, expected, verifier_key): assert decoded["ear.status"] == expected_claims["ear.status"] + if "ear.appraisal-policy-id" in expected_claims: + assert decoded["ear.appraisal-policy-id"] ==\ + expected_claims["ear.appraisal-policy-id"] + for trust_claim, tc_value in decoded["ear.trustworthiness-vector"].items(): expected_value = expected_claims["ear.trustworthiness-vector"][trust_claim] assert expected_value == tc_value, f'mismatch for claim "{trust_claim}"' diff --git a/policy/README.md b/policy/README.md index e2a445bc..02079496 100644 --- a/policy/README.md +++ b/policy/README.md @@ -31,3 +31,84 @@ The following policy agent configuration directives are currently supported: ### `opa` backend configuration Currently, `opa` backend does not support any configuration. + +## Policy Identification + +There are three different ways of identifying a policy: + +### appraisal policy ID + +This is an identifier used in the attestation result as defined by [EAR +Internet draft](https://www.ietf.org/archive/id/draft-fv-rats-ear-01.html#name-ear-appraisal-claims). +Note that in this case, "policy" is used in the sense of "Appraisal Policy for +Evidence" as per [RATS architecture](https://www.rfc-editor.org/rfc/rfc9334). +In Veraison, this encompasses both, the scheme, and the policy applied from the +policy store by a policy engine. + +An appraisal policy ID is a [URI](https://www.rfc-editor.org/rfc/rfc3986) with +the scheme `policy` followed by a rootless path indicating the (RATS) policy +using which the appraisal has been generated. The first segment of the path is +the name of the scheme used to create the appraisal. The second segment, if +present, is the individual policy ID (see below) of the policy that has been +applied to the appraisal created by the scheme. + +For example: + +- `policy:TPM_ENACTTRUST`: the appraisal has been created using "TPM_ENACTTRUST" scheme, with + no additional policy applied. +- `policy:PSA_IOT/340d22f7-9eda-499f-9aa2-5af295d6d812`: the appraisal has been + created using "PSA_IOT" scheme and has subsequently been updated by the + policy with unique policy ID "ae19cc27-a449-1fb8-6c10-00f47ad1c55c". + +#### Potential future extensions + +These indicate potential future enhancements, and are **not** supported by the +current implementation. + +##### Cascading policies + +In the future we may support applying multiple individual policies to a single +appraisal. In that case, each path segment after the first (the scheme) is the +individual policy ID of a policy that has been applied. The ordering of the +segments matches the order in which the policies were applied. + +For example: + +- `policy:PSA_IOT/340d22f7-9eda-499f-9aa2-5af295d6d812/ae19cc27-a449-1fb8-6c10-00f47ad1c55c`: + the appraisal has been created using "PSA_IOT" scheme, it was then updated by + a policy with the individual policy id + `340d22f7-9eda-499f-9aa2-5af295d6d812`, followed by a policy with the + individual policy ID `ae19cc27-a449-1fb8-6c10-00f47ad1c55c`. + +### policy store key + +Policies are stored, retrieved from, and updated in the policy store using a key. +The key is a string consisting of the tenant id, scheme, and policy name +delimited by colons. + +For example: + +- `0:PSA_IOT:opa`: the key for tenant "0"'s policy for scheme "PSA_IOT" with + name "opa". + +#### policy name + +The name exists to support cascading policies in the future (see above). At the +moment, as there is only one active policy allowed per appraisal, the name is +not necessary and is always set to the name of the policy engine ("opa"). While +this unnecessarily increases the key size and is somewhat wasteful, given that +the number of the policies a typical deployment is expected to be, at most, in +the hundreds, and the relatively negligible overhead compared to the size of +the policies themselves, this is not deemed to be a major concern. + +### individual policy ID + +The individual policy ID identifies the specific policy that was applied to an +appraisal. It forms a component of the appraisal policy ID (which also includes +the scheme, and possibly, in the future, individual IDs from multiple +policies). It differs from the policy store key in that it also incorporates +versioning information. + +The individual policy id is the UUID of the specific policy instance. + +For example: `340d22f7-9eda-499f-9aa2-5af295d6d812` diff --git a/policy/agent.go b/policy/agent.go index de2e7d1a..e0dabdbc 100644 --- a/policy/agent.go +++ b/policy/agent.go @@ -91,7 +91,7 @@ func (o *Agent) Evaluate( return nil, fmt.Errorf("could not evaluate policy: %w", err) } - o.logger.Debugw("policy evaluated", "policy-id", policy.ID, "updated", updatedByPolicy) + o.logger.Debugw("policy evaluated", "policy-id", policy.StoreKey, "updated", updatedByPolicy) updatedStatus, ok := updatedByPolicy["ear.status"] if !ok { @@ -126,7 +126,8 @@ func (o *Agent) Evaluate( if err != nil { return nil, fmt.Errorf("bad appraisal data from policy: %w", err) } - evaluatedAppraisal.AppraisalPolicyID = &policy.ID + evaluatedAppraisal.AppraisalPolicyID = appraisal.AppraisalPolicyID + return evaluatedAppraisal, nil } else { // policy did not update anything, so return the original appraisal diff --git a/policy/agent_test.go b/policy/agent_test.go index ff72dd3f..8de1f014 100644 --- a/policy/agent_test.go +++ b/policy/agent_test.go @@ -142,15 +142,17 @@ func Test_Agent_Evaluate(t *testing.T) { ctx := context.Background() policy := &Policy{ - ID: "test-policy", - Rules: "", + StoreKey: PolicyKey{"test-tenant", "test-scheme", "test-policy"}, + Rules: "", } var endorsements []string contraStatus := ear.TrustTierContraindicated + polID := "policy:test-scheme" appraisal := &ear.Appraisal{ - Status: &contraStatus, - TrustVector: &ear.TrustVector{}, + Status: &contraStatus, + TrustVector: &ear.TrustVector{}, + AppraisalPolicyID: &polID, } evidence := &proto.EvidenceContext{} @@ -184,7 +186,7 @@ func Test_Agent_Evaluate(t *testing.T) { if v.ExpectedAppraisal == nil { assert.Nil(t, res) } else { - assert.Equal(t, policy.ID, *res.AppraisalPolicyID) + assert.Equal(t, *appraisal.AppraisalPolicyID, *res.AppraisalPolicyID) assert.Equal(t, *v.ExpectedAppraisal.Status, *res.Status) assert.Equal(t, v.ExpectedAppraisal.TrustVector.InstanceIdentity, res.TrustVector.InstanceIdentity) diff --git a/policy/cmd/polcli/commands/add.go b/policy/cmd/polcli/commands/add.go index 26f51a3d..6456a489 100644 --- a/policy/cmd/polcli/commands/add.go +++ b/policy/cmd/polcli/commands/add.go @@ -34,7 +34,7 @@ func init() { func validateAddArgs(cmd *cobra.Command, args []string) error { // note: assumes ExactArgs(2) matched. - if err := policy.ValidateID(args[0]); err != nil { + if _, err := policy.PolicyKeyFromString(args[0]); err != nil { return fmt.Errorf("invalid policy ID: %w", err) } @@ -46,7 +46,11 @@ func validateAddArgs(cmd *cobra.Command, args []string) error { } func doAddCommand(cmd *cobra.Command, args []string) error { - policyID := args[0] + policyID, err := policy.PolicyKeyFromString(args[0]) + if err != nil { + return err + } + policyFile := args[1] rulesBytes, err := os.ReadFile(policyFile) @@ -59,11 +63,13 @@ func doAddCommand(cmd *cobra.Command, args []string) error { addFunc = store.Update } - if err := addFunc(policyID, string(rulesBytes)); err != nil { + policy, err := addFunc(policyID, "default", "opa", string(rulesBytes)) + if err != nil { return fmt.Errorf("could not add policy: %w", err) } - log.Printf("Policy %q stored under ID %q.\n", policyFile, policyID) + log.Printf("Policy %q stored under key %q with UUID %q .\n", + policyFile, policyID, policy.UUID) return nil } diff --git a/policy/cmd/polcli/commands/del.go b/policy/cmd/polcli/commands/del.go index 1e2d57a6..ba81973a 100644 --- a/policy/cmd/polcli/commands/del.go +++ b/policy/cmd/polcli/commands/del.go @@ -26,7 +26,7 @@ var ( func validateDelArgs(cmd *cobra.Command, args []string) error { // note: assumes ExactArgs(1) matched. - if err := policy.ValidateID(args[0]); err != nil { + if _, err := policy.PolicyKeyFromString(args[0]); err != nil { return fmt.Errorf("invalid policy ID: %w", err) } @@ -34,7 +34,10 @@ func validateDelArgs(cmd *cobra.Command, args []string) error { } func doDelCommand(cmd *cobra.Command, args []string) error { - policyID := args[0] + policyID, err := policy.PolicyKeyFromString(args[0]) + if err != nil { + return err + } if err := store.Del(policyID); err != nil { return fmt.Errorf("could not delete policy: %w", err) diff --git a/policy/cmd/polcli/commands/get.go b/policy/cmd/polcli/commands/get.go index 60a80d08..e17bb097 100644 --- a/policy/cmd/polcli/commands/get.go +++ b/policy/cmd/polcli/commands/get.go @@ -21,12 +21,12 @@ var ( PostRunE: finiPolicyStore, } - getVersion int32 + getUUID string getOutputFilePath string ) func init() { - getCmd.PersistentFlags().Int32VarP(&getVersion, "version", "v", 0, + getCmd.PersistentFlags().StringVarP(&getUUID, "version", "v", "", "get the specified, rather than latest, version") getCmd.PersistentFlags().StringVarP(&getOutputFilePath, "output", "o", "", "write the policy to the specified file, rather than STDOUT") @@ -35,7 +35,7 @@ func init() { func validateGetArgs(cmd *cobra.Command, args []string) error { // note: assumes ExactArgs(1) matched. - if err := policy.ValidateID(args[0]); err != nil { + if _, err := policy.PolicyKeyFromString(args[0]); err != nil { return fmt.Errorf("invalid policy ID: %w", err) } @@ -43,33 +43,38 @@ func validateGetArgs(cmd *cobra.Command, args []string) error { } func doGetCommand(cmd *cobra.Command, args []string) error { - var policies []policy.Policy - var policy policy.Policy + var policies []*policy.Policy + var pol *policy.Policy var err error - policyID := args[0] + policyKey, err := policy.PolicyKeyFromString(args[0]) + if err != nil { + return err + } - if getVersion == 0 { - policy, err = store.GetLatest(policyID) + if getUUID == "" { + pol, err = store.GetActive(policyKey) if err != nil { return err } } else { - policies, err = store.Get(policyID) + policies, err = store.Get(policyKey) if err != nil { return err } + found := false for _, candidate := range policies { - if candidate.Version == getVersion { - policy = candidate + if candidate.UUID.String() == getUUID { + pol = candidate + found = true break } } - if policy.Version == 0 { - return fmt.Errorf("version %d for policy %q not found", - getVersion, policyID) + if !found { + return fmt.Errorf("UUID %q for policy %q not found", + getUUID, policyKey) } } @@ -85,7 +90,7 @@ func doGetCommand(cmd *cobra.Command, args []string) error { writer = os.Stdout } - if _, err := writer.Write([]byte(policy.Rules)); err != nil { + if _, err := writer.Write([]byte(pol.Rules)); err != nil { return err } diff --git a/policy/cmd/polcli/commands/list.go b/policy/cmd/polcli/commands/list.go index 2c7fea09..f1c432e7 100644 --- a/policy/cmd/polcli/commands/list.go +++ b/policy/cmd/polcli/commands/list.go @@ -45,9 +45,9 @@ func doListCommand(cmd *cobra.Command, args []string) error { table.SetHeader([]string{"id", "version", "md5sum"}) for _, p := range policies { - version := fmt.Sprint(p.Version) + uuid := p.UUID.String() md5sum := fmt.Sprintf("%x", md5.Sum([]byte(p.Rules))) - table.Append([]string{p.ID, version, md5sum}) + table.Append([]string{p.StoreKey.String(), uuid, md5sum}) } table.Render() diff --git a/policy/id.go b/policy/id.go deleted file mode 100644 index 1b2c3c06..00000000 --- a/policy/id.go +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package policy - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -// Policy ID uniquely identifies the policy to be applied. It is currently -// assumed that, at a given point in time, there is at most one policy per -// tenant on a deployment (note: a tenant can implement multiple "logical -// policies" by defining alternate rules and switching between them in the -// top-level policy based on input). This means that a policy can effectively -// be identified (in the context of a deployment) by the tenant ID. -// Additionally, since the structure of a policy is specific to a particular -// policy agent, it is desirable to have that reflected in the ID in order to -// minimise the likelihood of issue in complex deployment that use multiple -// agents (potentially with different backends). -// With the above in mind, the policy ID is defined to be in the format -// :// - -// ValidateID returns nil if the provided string is a valid policy ID. -// Otherwise, an error indicating the problem is returned. -func ValidateID(in string) error { - parts := strings.Split(in, "://") - if len(parts) != 2 { - return errors.New("wrong format") - } - - if !IsValidAgentBackend(parts[0]) { - return fmt.Errorf("not a valid agent backend: %q", parts[0]) - } - - if _, err := strconv.Atoi(parts[1]); err != nil { - return fmt.Errorf("not a valid tenant ID: %q", parts[1]) - } - - return nil -} diff --git a/policy/policy.go b/policy/policy.go index c0d51889..d3e8da99 100644 --- a/policy/policy.go +++ b/policy/policy.go @@ -2,14 +2,60 @@ // SPDX-License-Identifier: Apache-2.0 package policy -// Policy allows enforcing additional constraints on top of the regular attestation schemes. +import ( + "time" + + "github.com/google/uuid" +) + +// Policy allows enforcing additional constraints on top of the regular +// attestation schemes. type Policy struct { - // ID is used to reference the policy in the result. - ID string `json:"id"` + // StoreKey is the identifier of this policy, unique to the store. + StoreKey PolicyKey `json:"-"` + + // UUID is the unque identifier associated with this specific instance + // of a policy. + UUID uuid.UUID `json:"uuid"` - // Version gets bumped every time a new policy with existing ID is added to the store. - Version int32 `json:"version"` + // CTime is the creationg time of this policy. + CTime time.Time `json:"ctime"` - // Rules of the policy to be interpreted and execute by the policy agent. + // Name is the name of this policy. It's a short descritor for the + // rules in this policy. + Name string `json:"name"` + + // Type identifies the policy engine used to evaluate the policy, and + // therfore dictates how the Rules should be interpreted. + Type string `json:"type"` + + // Rules of the policy to be interpreted and execute by the policy + // agent. Rules string `json:"rules"` + + // Active indicates whether this policy instance is currently active + // for the associated key. + Active bool `json:"active"` +} + +// NewPolicy creates a new Policy based on the specified PolicyID and rules. +func NewPolicy(key PolicyKey, name, typ, rules string) (*Policy, error) { + polUUID, err := uuid.NewUUID() + if err != nil { + return nil, err + } + + return &Policy{ + StoreKey: key, + UUID: polUUID, + CTime: time.Now(), + Type: typ, + Name: name, + Rules: rules, + }, nil +} + +// Validate returns an error if the current policy is invalid. +func (o *Policy) Validate() error { + return o.StoreKey.Validate() } diff --git a/policy/policy_key.go b/policy/policy_key.go new file mode 100644 index 00000000..157daffc --- /dev/null +++ b/policy/policy_key.go @@ -0,0 +1,63 @@ +// Copyright 2022-2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package policy + +import ( + "fmt" + "net/url" + "strings" +) + +// PolicyKey identifies a specific policy. This is used to retrieve the policy +// from the store. +type PolicyKey struct { + // TenantId is the ID of the tenant that owns this policy. + TenantId string `json:"tenant_id"` + + // Scheme is the name of the scheme with which this policy is associated + Scheme string `json:"scheme"` + + // Name is the name of this policy + Name string `json:"name"` +} + +// PolicyKeyFromString parses the specified string containing a policy store key +// into a PolicyID. +func PolicyKeyFromString(s string) (PolicyKey, error) { + var key PolicyKey + + parts := strings.Split(s, ":") + if len(parts) != 3 { + return key, fmt.Errorf( + "bad policy store key %q: want 3 :-separated parts, found %d", + s, len(parts), + ) + } + + key.TenantId = parts[0] + key.Scheme = parts[1] + key.Name = parts[2] + + return key, key.Validate() +} + +func (o PolicyKey) Validate() error { + if url.PathEscape(o.TenantId) != o.TenantId { + return fmt.Errorf("bad TenantId %q: must be a valid URI path segment", o.TenantId) + } + + if url.PathEscape(o.Scheme) != o.Scheme { + return fmt.Errorf("bad Scheme %q: must be a valid URI path segment", o.Scheme) + } + + if url.PathEscape(o.Name) != o.Name { + return fmt.Errorf("bad Name %q: must be a valid URI path segment", o.Name) + } + + return nil +} + +// String returns the string representation of the PolicyKey. +func (o PolicyKey) String() string { + return fmt.Sprintf("%s:%s:%s", o.TenantId, o.Scheme, o.Name) +} diff --git a/policy/policy_key_test.go b/policy/policy_key_test.go new file mode 100644 index 00000000..987607b9 --- /dev/null +++ b/policy/policy_key_test.go @@ -0,0 +1,37 @@ +// Copyright 2022-2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package policy + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_PolicyKey(t *testing.T) { + storeKey := "test-tenant:test-scheme:test-name" + + key, err := PolicyKeyFromString(storeKey) + require.NoError(t, err) + assert.Equal(t, "test-tenant", key.TenantId) + assert.Equal(t, "test-scheme", key.Scheme) + assert.Equal(t, "test-name", key.Name) + assert.Equal(t, storeKey, key.String()) + + _, err = PolicyKeyFromString("bad:id") + assert.EqualError(t, err, + "bad policy store key \"bad:id\": want 3 :-separated parts, found 2") + + _, err = PolicyKeyFromString("tenant/1:scheme:name") + assert.EqualError(t, err, + "bad TenantId \"tenant/1\": must be a valid URI path segment") + + _, err = PolicyKeyFromString("0:bad%scheme:name") + assert.EqualError(t, err, + "bad Scheme \"bad%scheme\": must be a valid URI path segment") + + _, err = PolicyKeyFromString("tenant:scheme:name<") + assert.EqualError(t, err, + "bad Name \"name<\": must be a valid URI path segment") +} diff --git a/policy/store.go b/policy/store.go index e500641f..d6dc9d51 100644 --- a/policy/store.go +++ b/policy/store.go @@ -3,16 +3,19 @@ package policy import ( + "bytes" "encoding/json" "errors" "fmt" + "github.com/google/uuid" "github.com/spf13/viper" "github.com/veraison/services/kvstore" "go.uber.org/zap" ) var ErrNoPolicy = errors.New("no policy found") +var ErrNoActivePolicy = errors.New("no active policy for key") // NewStore returns a new policy store. Config options are the same as those // used for kvstore.New(). @@ -38,51 +41,37 @@ func (o *Store) Setup() error { // Add a policy with the specified ID and rules. If a policy with that ID // already exists, an error is returned. -func (o *Store) Add(id, rules string) error { +func (o *Store) Add(id PolicyKey, name, typ, rules string) (*Policy, error) { if _, err := o.Get(id); err == nil { - return fmt.Errorf("policy with id %q already exists", id) + return nil, fmt.Errorf("policy with id %q already exists", id) } - return o.Update(id, rules) + return o.Update(id, name, typ, rules) } // Update sets the provided rules as the latest version of the policy with the -// specified ID. If a policy with that ID does not exist, it is created. -func (o *Store) Update(id, rules string) error { - var oldVersion int32 - - oldPolicy, err := o.GetLatest(id) - - if err == nil { // nolint:gocritic - oldVersion = oldPolicy.Version - } else if errors.Is(err, ErrNoPolicy) { - oldVersion = 0 - } else { - return err - } - - newPolicy := Policy{ID: id, Rules: rules, Version: oldVersion + 1} - - newPolicyBytes, err := json.Marshal(newPolicy) +// specified key. If a policy with that key does not exist, it is created. +func (o *Store) Update(key PolicyKey, name, typ, rules string) (*Policy, error) { + newPolicy, err := NewPolicy(key, name, typ, rules) if err != nil { - return err + return newPolicy, err } - return o.KVStore.Add(id, string(newPolicyBytes)) + return newPolicy, o.addPolicy(newPolicy) } -// Get returns the slice of all Policies associated with he specified ID. Each +// Get returns the slice of all Policies associated with the specified ID. Each // Policy represents a different version of the same logical policy. -func (o *Store) Get(id string) ([]Policy, error) { - vals, err := o.KVStore.Get(id) +func (o *Store) Get(key PolicyKey) ([]*Policy, error) { + vals, err := o.KVStore.Get(key.String()) if err != nil { if errors.Is(err, kvstore.ErrKeyNotFound) { - return nil, fmt.Errorf("%w: %q", ErrNoPolicy, id) + return nil, fmt.Errorf("%w: %q", ErrNoPolicy, key) } return nil, err } - var policies []Policy // nolint:prealloc + var policies []*Policy // nolint:prealloc for _, v := range vals { var p Policy @@ -90,7 +79,8 @@ func (o *Store) Get(id string) ([]Policy, error) { return nil, err } - policies = append(policies, p) + p.StoreKey = key + policies = append(policies, &p) } return policies, nil @@ -99,16 +89,20 @@ func (o *Store) Get(id string) ([]Policy, error) { // List returns []Policy containing latest versions of all policies. All // policies returned will have distinct IDs. In cases where multiple policies // exist for one ID in the store, the latest version will be returned. -func (o *Store) List() ([]Policy, error) { - keys, err := o.KVStore.GetKeys() +func (o *Store) List() ([]*Policy, error) { + keys, err := o.GetPolicyKeys() if err != nil { return nil, err } - var policies []Policy // nolint:prealloc + var policies []*Policy // nolint:prealloc for _, key := range keys { - policy, err := o.GetLatest(key) + policy, err := o.GetActive(key) if err != nil { + if errors.Is(err, ErrNoActivePolicy) { + continue + } + return nil, err } @@ -121,13 +115,13 @@ func (o *Store) List() ([]Policy, error) { // ListAllVersions returns a []Policy containing every policy entry in the // underlying store, including multiple versions associated with a single // policy ID. -func (o *Store) ListAllVersions() ([]Policy, error) { - keys, err := o.KVStore.GetKeys() +func (o *Store) ListAllVersions() ([]*Policy, error) { + keys, err := o.GetPolicyKeys() if err != nil { return nil, err } - var policies []Policy + var policies []*Policy for _, key := range keys { versions, err := o.Get(key) if err != nil { @@ -140,23 +134,135 @@ func (o *Store) ListAllVersions() ([]Policy, error) { return policies, nil } -// GetLatest returns the latest version of the policy with the specified ID. If -// no such policy exists, a wrapped ErrNoPolicy is returned. -func (o *Store) GetLatest(id string) (Policy, error) { - policies, err := o.Get(id) +// GetPolicyKeys returns a []PolicyID of the policies currently in the store. +func (o *Store) GetPolicyKeys() ([]PolicyKey, error) { + keys, err := o.KVStore.GetKeys() + if err != nil { + return nil, err + } + + ids := make([]PolicyKey, len(keys)) + for i, k := range keys { + key, err := PolicyKeyFromString(k) + if err != nil { + return nil, fmt.Errorf("bad key in store: %w", err) + } + + ids[i] = key + } + + return ids, nil +} + +// Activate activates the policy version with the specified id for the +// specified key. +func (o *Store) Activate(key PolicyKey, id uuid.UUID) error { + policies, err := o.Get(key) + if err != nil { + return err + } + + activated := false + for _, pol := range policies { + if bytes.Equal(id[:], pol.UUID[:]) { + pol.Active = true + activated = true + } else { + pol.Active = false + } + } + + if !activated { + return fmt.Errorf("%w with UUID %q for key %q", ErrNoPolicy, id, key.String()) + } + + if err := o.Del(key); err != nil { + return err + } + + for _, pol := range policies { + if err := o.addPolicy(pol); err != nil { + return err + } + } + + return nil +} + +// DeactivateAll deactivates all policies associated with the key. +func (o *Store) DeactivateAll(key PolicyKey) error { + policies, err := o.Get(key) + if err != nil { + return err + } + + for _, pol := range policies { + pol.Active = false + } + + if err := o.Del(key); err != nil { + return err + } + + for _, pol := range policies { + if err := o.addPolicy(pol); err != nil { + return err + } + } + + return nil +} + +// GetActive returns the current active version of the policy with the +// specified key, or an error if no such policy exists. +func (o *Store) GetActive(key PolicyKey) (*Policy, error) { + policies, err := o.Get(key) + if err != nil { + return nil, err + } + + for _, pol := range policies { + if pol.Active { + return pol, nil + } + } + + return nil, fmt.Errorf("%w %q", ErrNoActivePolicy, key.String()) +} + +// GetPolicy returns the policy with the specified UUID under the specified +// key. +func (o *Store) GetPolicy(key PolicyKey, id uuid.UUID) (*Policy, error) { + policies, err := o.Get(key) if err != nil { - return Policy{}, err + return nil, err } - return policies[len(policies)-1], nil + for _, pol := range policies { + if bytes.Equal(id[:], pol.UUID[:]) { + return pol, nil + } + } + + return nil, fmt.Errorf("%w with UUID %q under key %q", + ErrNoPolicy, id.String(), key.String()) } -// Del removes all policy versisions associated with the specfied id. -func (o *Store) Del(id string) error { - return o.KVStore.Del(id) +// Del removes all policy versions associated with the specified key. +func (o *Store) Del(key PolicyKey) error { + return o.KVStore.Del(key.String()) } // Close the connection to the underlying kvstore. func (o *Store) Close() error { return o.KVStore.Close() } + +func (o *Store) addPolicy(policy *Policy) error { + policyBytes, err := json.Marshal(policy) + if err != nil { + return err + } + + return o.KVStore.Add(policy.StoreKey.String(), string(policyBytes)) +} diff --git a/policy/store_test.go b/policy/store_test.go index e2a2dcfc..807df56b 100644 --- a/policy/store_test.go +++ b/policy/store_test.go @@ -19,46 +19,71 @@ func Test_Store_CRUD(t *testing.T) { require.NoError(t, err) defer store.Close() - err = store.Add("p1", "1. the chief's always right; 2. if the chief's wrong, see 1.") + key := PolicyKey{"1", "scheme", "policy"} + + policy, err := store.Add(key, "test", "test", + "1. the chief's always right; 2. if the chief's wrong, see 1.") require.NoError(t, err) - policy, err := store.GetLatest("p1") + _, err = store.GetActive(key) + assert.EqualError(t, err, "no active policy for key \"1:scheme:policy\"") + + err = store.Activate(key, policy.UUID) require.NoError(t, err) - assert.Equal(t, "p1", policy.ID) - assert.Equal(t, int32(1), policy.Version) + policy, err = store.GetActive(key) + require.NoError(t, err) + assert.Equal(t, key, policy.StoreKey) - err = store.Add("p1", "On second thought, chief's not always right.") + _, err = store.Add(key, "test", "test", "On second thought, chief's not always right.") assert.ErrorContains(t, err, "already exists") - err = store.Update("p1", "On second thought, chief's not always right.") + secondPolicy, err := store.Update(key, "test", "test", + "On second thought, chief's not always right.") + require.NoError(t, err) + + policy, err = store.GetActive(key) + require.NoError(t, err) + assert.Equal(t, "1. the chief's always right; 2. if the chief's wrong, see 1.", policy.Rules) + + err = store.Activate(key, secondPolicy.UUID) require.NoError(t, err) - policy, err = store.GetLatest("p1") + policy, err = store.GetActive(key) require.NoError(t, err) - assert.Equal(t, int32(2), policy.Version) assert.Equal(t, "On second thought, chief's not always right.", policy.Rules) - versions, err := store.Get("p1") + err = store.DeactivateAll(key) + require.NoError(t, err) + + _, err = store.GetActive(key) + assert.EqualError(t, err, "no active policy for key \"1:scheme:policy\"") + + versions, err := store.Get(key) require.NoError(t, err) assert.Len(t, versions, 2) - assert.Equal(t, int32(2), versions[1].Version) policies, err := store.List() require.NoError(t, err) + assert.Equal(t, 0, len(policies)) + + err = store.Activate(key, secondPolicy.UUID) + require.NoError(t, err) + + policies, err = store.List() + require.NoError(t, err) assert.Equal(t, 1, len(policies)) - assert.Equal(t, "p1", policies[0].ID) + assert.Equal(t, key, policies[0].StoreKey) policies, err = store.ListAllVersions() require.NoError(t, err) assert.Equal(t, 2, len(policies)) - assert.Equal(t, "p1", policies[0].ID) - assert.Equal(t, int32(1), policies[0].Version) - assert.Equal(t, int32(2), policies[1].Version) + assert.Equal(t, key, policies[0].StoreKey) + assert.NotEqual(t, policies[0].UUID, policies[1].UUID) - err = store.Del("p1") + err = store.Del(key) require.NoError(t, err) - _, err = store.GetLatest("p1") + _, err = store.GetActive(key) assert.ErrorIs(t, err, ErrNoPolicy) } diff --git a/policy/test/inputs/psa-result.json b/policy/test/inputs/psa-result.json index e0cd39ae..a5b2f3df 100644 --- a/policy/test/inputs/psa-result.json +++ b/policy/test/inputs/psa-result.json @@ -18,6 +18,7 @@ "storage-opaque": 0, "sourced-data": 0 }, + "ear.appraisal-policy-id": "policy:PSA_OIT", "ear.veraison.annotated-evidence": { "psa-boot-seed": "BwYFBAMCAQAPDg0MCwoJCBcWFRQTEhEQHx4dHBsaGRg=", "psa-client-id": 2, diff --git a/vts/appraisal/appraisal.go b/vts/appraisal/appraisal.go index c7002a92..700b1133 100644 --- a/vts/appraisal/appraisal.go +++ b/vts/appraisal/appraisal.go @@ -5,9 +5,12 @@ package appraisal import ( "encoding/base64" + "fmt" + "strings" "github.com/veraison/ear" "github.com/veraison/services/config" + "github.com/veraison/services/policy" "github.com/veraison/services/proto" ) @@ -15,17 +18,19 @@ import ( // policy evaluation). It is the analog of proto.AppraisalContext, but with a // deserialized AttestationResult. type Appraisal struct { + Scheme string EvidenceContext *proto.EvidenceContext Result *ear.AttestationResult SignedEAR []byte } -func New(tenantID string, nonce []byte, submodName string) *Appraisal { +func New(tenantID string, nonce []byte, scheme string) *Appraisal { appraisal := Appraisal{ + Scheme: scheme, EvidenceContext: &proto.EvidenceContext{ TenantId: tenantID, }, - Result: ear.NewAttestationResult(submodName, config.Version, config.Developer), + Result: ear.NewAttestationResult(scheme, config.Version, config.Developer), } encodedNonce := base64.URLEncoding.EncodeToString(nonce) @@ -34,6 +39,8 @@ func New(tenantID string, nonce []byte, submodName string) *Appraisal { appraisal.Result.VerifierID.Build = &config.Version appraisal.Result.VerifierID.Developer = &config.Developer + appraisal.InitPolicyID() + return &appraisal } @@ -59,3 +66,25 @@ func (o Appraisal) AddPolicyClaim(name, claim string) { (*submod.AppraisalExtensions.VeraisonPolicyClaims)[name] = claim } } + +func (o *Appraisal) UpdatePolicyID(pol *policy.Policy) error { + if err := pol.Validate(); err != nil { + return err + } + + subID := pol.UUID.String() + + for _, submod := range o.Result.Submods { + updatedID := strings.Join([]string{*submod.AppraisalPolicyID, subID}, "/") + submod.AppraisalPolicyID = &updatedID + } + + return nil +} + +func (o *Appraisal) InitPolicyID() { + for _, submod := range o.Result.Submods { + policyID := fmt.Sprintf("policy:%s", o.Scheme) + submod.AppraisalPolicyID = &policyID + } +} diff --git a/vts/policymanager/policymanager.go b/vts/policymanager/policymanager.go index 5ad0d993..a787f197 100644 --- a/vts/policymanager/policymanager.go +++ b/vts/policymanager/policymanager.go @@ -5,11 +5,9 @@ package policymanager import ( "context" "errors" - "fmt" "github.com/spf13/viper" "github.com/veraison/services/policy" - "github.com/veraison/services/proto" "github.com/veraison/services/vts/appraisal" "go.uber.org/zap" ) @@ -40,13 +38,12 @@ func (o *PolicyManager) Evaluate( appraisal *appraisal.Appraisal, endorsements []string, ) error { - evidence := appraisal.EvidenceContext - policyID := o.getPolicyID(evidence) + policyKey := o.getPolicyKey(appraisal) - pol, err := o.getPolicy(policyID) + pol, err := o.getPolicy(policyKey) if err != nil { if errors.Is(err, policy.ErrNoPolicy) { - o.logger.Debugw("no policy", "policy-id", policyID) + o.logger.Debugw("no policy", "policy-id", policyKey) return nil // No policy? No problem! } @@ -55,29 +52,33 @@ func (o *PolicyManager) Evaluate( for submod, submodAppraisal := range appraisal.Result.Submods { evaluated, err := o.Agent.Evaluate( - ctx, scheme, pol, submod, submodAppraisal, evidence, endorsements) + ctx, scheme, pol, submod, submodAppraisal, appraisal.EvidenceContext, endorsements) if err != nil { return err } appraisal.Result.Submods[submod] = evaluated + + if err := appraisal.UpdatePolicyID(pol); err != nil { + return err + } } return nil } -func (o *PolicyManager) getPolicyID(ec *proto.EvidenceContext) string { - return fmt.Sprintf("%s://%s", - o.Agent.GetBackendName(), - ec.TenantId, - ) - +func (o *PolicyManager) getPolicyKey(a *appraisal.Appraisal) policy.PolicyKey { + return policy.PolicyKey{ + TenantId: a.EvidenceContext.TenantId, + Scheme: a.Scheme, + Name: o.Agent.GetBackendName(), + } } -func (o *PolicyManager) getPolicy(policyID string) (*policy.Policy, error) { - p, err := o.Store.GetLatest(policyID) +func (o *PolicyManager) getPolicy(policyKey policy.PolicyKey) (*policy.Policy, error) { + p, err := o.Store.GetActive(policyKey) if err != nil { return nil, err } - return &p, nil + return p, nil } diff --git a/vts/policymanager/policymanager_test.go b/vts/policymanager/policymanager_test.go index bb1fb76d..1d1cc5e2 100644 --- a/vts/policymanager/policymanager_test.go +++ b/vts/policymanager/policymanager_test.go @@ -26,7 +26,7 @@ func TestPolicyMgr_getPolicy_not_found(t *testing.T) { store := mock_deps.NewMockIKVStore(ctrl) store.EXPECT(). - Get(gomock.Eq("opa://0")). + Get(gomock.Eq("0:TPM_ENACTTRUST:opa")). Return(nil, kvstore.ErrKeyNotFound) // Get the Mock Agent here @@ -35,20 +35,23 @@ func TestPolicyMgr_getPolicy_not_found(t *testing.T) { evStruct, err := structpb.NewStruct(nil) require.NoError(t, err) - ec := &proto.EvidenceContext{ - TenantId: "0", - TrustAnchorId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", - ReferenceId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", - Evidence: evStruct, + appraisal := &appraisal.Appraisal{ + Scheme: "TPM_ENACTTRUST", + EvidenceContext: &proto.EvidenceContext{ + TenantId: "0", + TrustAnchorId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", + ReferenceId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", + Evidence: evStruct, + }, } pm := &PolicyManager{Store: &policy.Store{KVStore: store, Logger: log.Named("test")}, Agent: agent} - polID := pm.getPolicyID(ec) - assert.Equal(t, "opa://0", polID) + polKey := pm.getPolicyKey(appraisal) + assert.Equal(t, "0:TPM_ENACTTRUST:opa", polKey.String()) - pol, err := pm.getPolicy(polID) + pol, err := pm.getPolicy(polKey) assert.Nil(t, pol) assert.ErrorIs(t, err, policy.ErrNoPolicy) } @@ -58,27 +61,31 @@ func TestPolicyMgr_getPolicy_OK(t *testing.T) { store := mock_deps.NewMockIKVStore(ctrl) store.EXPECT(). - Get(gomock.Eq("opa://0")). - Return([]string{"{}"}, nil) + Get(gomock.Eq("0:TPM_ENACTTRUST:opa")). + Return([]string{`{"uuid": "7df7714e-aa04-4638-bcbf-434b1dd720f1", "active": true}`}, nil) agent := mock_deps.NewMockIAgent(ctrl) agent.EXPECT().GetBackendName().Return("opa") + evStruct, err := structpb.NewStruct(nil) require.NoError(t, err) - ec := &proto.EvidenceContext{ - TenantId: "0", - TrustAnchorId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", - ReferenceId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", - Evidence: evStruct, + appraisal := &appraisal.Appraisal{ + Scheme: "TPM_ENACTTRUST", + EvidenceContext: &proto.EvidenceContext{ + TenantId: "0", + TrustAnchorId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", + ReferenceId: "TPM_ENACTTRUST://0/7df7714e-aa04-4638-bcbf-434b1dd720f1", + Evidence: evStruct, + }, } pm := &PolicyManager{Store: &policy.Store{KVStore: store}, Agent: agent} - polID := pm.getPolicyID(ec) - assert.Equal(t, "opa://0", polID) + polKey := pm.getPolicyKey(appraisal) + assert.Equal(t, "0:TPM_ENACTTRUST:opa", polKey.String()) - _, err = pm.getPolicy(polID) + _, err = pm.getPolicy(polKey) require.NoError(t, err) } @@ -110,8 +117,8 @@ func TestPolicyMgr_Evaluate_OK(t *testing.T) { store := mock_deps.NewMockIKVStore(ctrl) store.EXPECT(). - Get(gomock.Eq("opa://0")). - Return([]string{"{}"}, nil) + Get(gomock.Eq("0:TPM_ENACTTRUST:opa")). + Return([]string{`{"uuid": "7df7714e-aa04-4638-bcbf-434b1dd720f1", "active": true}`}, nil) ec := &proto.EvidenceContext{ TenantId: "0", @@ -121,12 +128,30 @@ func TestPolicyMgr_Evaluate_OK(t *testing.T) { } endorsements := []string{"h0KPxSKAPTEGXnvOPPA/5HUJZjHl4Hu9eg/eYMTPJcc="} ar := ear.NewAttestationResult("test", "test", "test") - ap := &appraisal.Appraisal{EvidenceContext: ec, Result: ar} + ap := &appraisal.Appraisal{EvidenceContext: ec, Result: ar, Scheme: "TPM_ENACTTRUST"} + + polID := "policy:TPM_ENACTTRUST" + tier := ear.TrustTierAffirming + earAp := ear.Appraisal{Status: &tier, AppraisalPolicyID: &polID} agent := mock_deps.NewMockIAgent(ctrl) agent.EXPECT().GetBackendName().Return("opa") - agent.EXPECT().Evaluate(context.TODO(), gomock.Any(), gomock.Any(), "test", ar.Submods["test"], ec, endorsements) - pm := &PolicyManager{Store: &policy.Store{KVStore: store}, Agent: agent} + agent.EXPECT(). + Evaluate( + context.TODO(), + gomock.Any(), + gomock.Any(), + "test", + ar.Submods["test"], + ec, + endorsements, + ). + Return(&earAp, nil) + pm := &PolicyManager{ + Store: &policy.Store{KVStore: store, Logger: log.Named("store")}, + Agent: agent, + logger: log.Named("manager"), + } err := pm.Evaluate(context.TODO(), "test", ap, endorsements) require.NoError(t, err) } @@ -137,8 +162,8 @@ func TestPolicyMgr_Evaluate_NOK(t *testing.T) { store := mock_deps.NewMockIKVStore(ctrl) store.EXPECT(). - Get(gomock.Eq("opa://0")). - Return([]string{"{}"}, nil) + Get(gomock.Eq("0:TPM_ENACTTRUST:opa")). + Return([]string{`{"uuid": "7df7714e-aa04-4638-bcbf-434b1dd720f1", "active": true}`}, nil) ec := &proto.EvidenceContext{ TenantId: "0", @@ -148,12 +173,16 @@ func TestPolicyMgr_Evaluate_NOK(t *testing.T) { } endorsements := []string{"h0KPxSKAPTEGXnvOPPA/5HUJZjHl4Hu9eg/eYMTPJcc="} ar := ear.NewAttestationResult("test", "test", "test") - ap := &appraisal.Appraisal{EvidenceContext: ec, Result: ar} + ap := &appraisal.Appraisal{EvidenceContext: ec, Result: ar, Scheme: "TPM_ENACTTRUST"} expectedErr := errors.New("could not evaluate policy: policy returned bad update") agent := mock_deps.NewMockIAgent(ctrl) agent.EXPECT().GetBackendName().Return("opa") agent.EXPECT().Evaluate(context.TODO(), gomock.Any(), gomock.Any(), "test", ar.Submods["test"], ec, endorsements).Return(nil, expectedErr) - pm := &PolicyManager{Store: &policy.Store{KVStore: store}, Agent: agent} + pm := &PolicyManager{ + Store: &policy.Store{KVStore: store, Logger: log.Named("store")}, + Agent: agent, + logger: log.Named("manager"), + } err := pm.Evaluate(context.TODO(), "test", ap, endorsements) assert.ErrorIs(t, err, expectedErr) diff --git a/vts/trustedservices/trustedservices_grpc.go b/vts/trustedservices/trustedservices_grpc.go index 78a3412a..1c51d296 100644 --- a/vts/trustedservices/trustedservices_grpc.go +++ b/vts/trustedservices/trustedservices_grpc.go @@ -449,6 +449,7 @@ func (o *GRPC) GetAttestation( return o.finalize(appraisal, err) } appraisal.Result = appraisedResult + appraisal.InitPolicyID() err = o.PolicyManager.Evaluate(ctx, handler.GetAttestationScheme(), appraisal, endorsements) if err != nil { From f8a04200907abf4e9fb1674697234d9373c3dc9e Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Thu, 29 Jun 2023 15:26:37 +0100 Subject: [PATCH 07/11] policy: add Agent.Validate() method Add a method to validate the policy rules without actually evaluating them against a particular input. For OPA, this validation amounts to a syntax check of the specified rules. This is intended for use by the forthcoming policy management API implementation. Signed-off-by: Sergei Trofimov --- policy/agent.go | 9 ++++ policy/iagent.go | 1 + policy/ibackend.go | 1 + policy/mocks/mock_ibackend.go | 14 ++++++ policy/opa.go | 15 +++++- policy/opa_test.go | 47 +++++++++++++++++-- .../{vectors.json => evaluate-vectors.json} | 0 policy/test/policies/null.rego | 0 policy/test/policies/simple.rego | 3 ++ policy/test/policies/undefined.rego | 3 ++ policy/test/validate-vectors.json | 37 +++++++++++++++ vts/policymanager/mocks/iagent.go | 14 ++++++ vts/policymanager/mocks/ibackend.go | 14 ++++++ 13 files changed, 153 insertions(+), 5 deletions(-) rename policy/test/{vectors.json => evaluate-vectors.json} (100%) create mode 100644 policy/test/policies/null.rego create mode 100644 policy/test/policies/simple.rego create mode 100644 policy/test/policies/undefined.rego create mode 100644 policy/test/validate-vectors.json diff --git a/policy/agent.go b/policy/agent.go index e0dabdbc..73930bce 100644 --- a/policy/agent.go +++ b/policy/agent.go @@ -135,6 +135,15 @@ func (o *Agent) Evaluate( } } +// Validate performs basic validation of the provided policy rules, returning +// an error if it fails. the nature of the validation performed is +// backend-specific, however it would typically amount to a syntax check. +// Successful validation does not guarantee that the policy will execute +// correctly againt actual inputs. +func (o *Agent) Validate(ctx context.Context, policyRules string) error { + return o.Backend.Validate(ctx, policyRules) +} + func (o *Agent) GetBackend() IBackend { return o.Backend } diff --git a/policy/iagent.go b/policy/iagent.go index 52371b3d..42ed3061 100644 --- a/policy/iagent.go +++ b/policy/iagent.go @@ -21,5 +21,6 @@ type IAgent interface { evidence *proto.EvidenceContext, endorsements []string, ) (*ear.Appraisal, error) + Validate(ctx context.Context, policyRules string) error Close() } diff --git a/policy/ibackend.go b/policy/ibackend.go index 43c2322f..e664cadd 100644 --- a/policy/ibackend.go +++ b/policy/ibackend.go @@ -19,5 +19,6 @@ type IBackend interface { evidence map[string]interface{}, endorsements []string, ) (map[string]interface{}, error) + Validate(ctx context.Context, policy string) error Close() } diff --git a/policy/mocks/mock_ibackend.go b/policy/mocks/mock_ibackend.go index 486b7e4b..a6cdc0ab 100644 --- a/policy/mocks/mock_ibackend.go +++ b/policy/mocks/mock_ibackend.go @@ -89,3 +89,17 @@ func (mr *MockIBackendMockRecorder) Init(v interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIBackend)(nil).Init), v) } + +// Validate mocks base method. +func (m *MockIBackend) Validate(ctx context.Context, policy string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", ctx, policy) + ret0, _ := ret[0].(error) + return ret0 +} + +// Validate indicates an expected call of Validate. +func (mr *MockIBackendMockRecorder) Validate(ctx, policy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockIBackend)(nil).Validate), ctx, policy) +} diff --git a/policy/opa.go b/policy/opa.go index 637465ee..c62742f1 100644 --- a/policy/opa.go +++ b/policy/opa.go @@ -56,7 +56,7 @@ func (o *OPA) Evaluate( rego := rego.New( rego.Package("policy"), rego.Module("opa.rego", preambleText), - rego.Module("policy.rego", string(policy)), + rego.Module("policy.rego", policy), rego.Input(input), rego.Query("outcome"), rego.Dump(log.Writer()), @@ -78,6 +78,19 @@ func (o *OPA) Evaluate( return resultUpdate, nil } +func (o *OPA) Validate(ctx context.Context, policy string) error { + rego := rego.New( + rego.Package("policy"), + rego.Module("opa.rego", preambleText), + rego.Module("policy.rego", policy), + rego.Query("outcome"), + rego.Dump(log.NamedWriter("opa", log.DebugLevel)), + ) + + _, err := rego.Compile(ctx) + return err +} + func (o *OPA) Close() { } diff --git a/policy/opa_test.go b/policy/opa_test.go index a73f509e..46774199 100644 --- a/policy/opa_test.go +++ b/policy/opa_test.go @@ -20,7 +20,7 @@ type TestResult struct { Outcome *ear.AttestationResult `json:"outcome"` } -type TestVector struct { +type EvaluateTestVector struct { Title string `json:"title"` Scheme string `json:"scheme"` ResultPath string `json:"result"` @@ -30,7 +30,7 @@ type TestVector struct { Expected TestResult `json:"expected"` } -func (o TestVector) Run(t *testing.T, ctx context.Context, pa *OPA) { +func (o EvaluateTestVector) Run(t *testing.T, ctx context.Context, pa *OPA) { resultMap, err := jsonFileToResultMap(o.ResultPath) require.NoError(t, err) @@ -54,6 +54,24 @@ func (o TestVector) Run(t *testing.T, ctx context.Context, pa *OPA) { assert.Equal(t, expected, res) } +type ValidateTestVector struct { + Title string `json:"title"` + PolicyPath string `json:"policy"` + Error string `json:"error"` +} + +func (o ValidateTestVector) Run(t *testing.T, ctx context.Context, pa *OPA) { + policy, err := os.ReadFile(o.PolicyPath) + require.NoError(t, err) + + err = pa.Validate(ctx, string(policy)) + if o.Error == "" { + assert.NoError(t, err) + } else { + assert.EqualError(t, err, o.Error) + } +} + func Test_OPA_GetName(t *testing.T) { pa, err := NewOPA(nil) require.NoError(t, err) @@ -63,7 +81,7 @@ func Test_OPA_GetName(t *testing.T) { } func Test_OPA_Evaluate(t *testing.T) { - bytes, err := os.ReadFile("test/vectors.json") + bytes, err := os.ReadFile("test/evaluate-vectors.json") require.NoError(t, err) ctx := context.Background() @@ -72,7 +90,7 @@ func Test_OPA_Evaluate(t *testing.T) { require.NoError(t, err) defer pa.Close() - var vectors []TestVector + var vectors []EvaluateTestVector err = json.Unmarshal(bytes, &vectors) require.NoError(t, err) @@ -94,6 +112,27 @@ func Test_OPA_Evaluate(t *testing.T) { } +func Test_OPA_Validate(t *testing.T) { + bytes, err := os.ReadFile("test/validate-vectors.json") + require.NoError(t, err) + + ctx := context.Background() + + pa, err := NewOPA(nil) + require.NoError(t, err) + defer pa.Close() + + var vectors []ValidateTestVector + + err = json.Unmarshal(bytes, &vectors) + require.NoError(t, err) + + for _, v := range vectors { + fmt.Printf("running %q\n", v.Title) + v.Run(t, ctx, pa) + } +} + func jsonFileToMap(path string) (map[string]interface{}, error) { bytes, err := os.ReadFile(path) if err != nil { diff --git a/policy/test/vectors.json b/policy/test/evaluate-vectors.json similarity index 100% rename from policy/test/vectors.json rename to policy/test/evaluate-vectors.json diff --git a/policy/test/policies/null.rego b/policy/test/policies/null.rego new file mode 100644 index 00000000..e69de29b diff --git a/policy/test/policies/simple.rego b/policy/test/policies/simple.rego new file mode 100644 index 00000000..e2eaab93 --- /dev/null +++ b/policy/test/policies/simple.rego @@ -0,0 +1,3 @@ +package policy + +executables = APPROVED_RT diff --git a/policy/test/policies/undefined.rego b/policy/test/policies/undefined.rego new file mode 100644 index 00000000..465533d8 --- /dev/null +++ b/policy/test/policies/undefined.rego @@ -0,0 +1,3 @@ +package policy + +executables = ok diff --git a/policy/test/validate-vectors.json b/policy/test/validate-vectors.json new file mode 100644 index 00000000..9bf37f7e --- /dev/null +++ b/policy/test/validate-vectors.json @@ -0,0 +1,37 @@ +[ + { + "title": "malformed policy", + "policy": "test/policies/malformed.rego", + "error": "1 error occurred: 1 error occurred: policy.rego:1: rego_parse_error: unexpected : token\n\tbad_rule:;;\n\t ^" + }, + { + "title": "bad policy", + "policy": "test/policies/bad.rego", + "error": "1 error occurred: policy.rego:6: rego_unsafe_var_error: var y is unsafe" + }, + { + "title": "empty policy", + "policy": "test/policies/empty.rego", + "error": null + }, + { + "title": "empty string", + "policy": "test/policies/null.rego", + "error": "1 error occurred: policy.rego:0: rego_parse_error: empty module" + }, + { + "title": "undefined variable", + "policy": "test/policies/undefined.rego", + "error": "1 error occurred: policy.rego:3: rego_unsafe_var_error: var ok is unsafe" + }, + { + "title": "valid simple", + "policy": "test/policies/simple.rego", + "error": null + }, + { + "title": "valid complex", + "policy": "test/policies/sw-up-to-dateness.rego", + "error": null + } +] diff --git a/vts/policymanager/mocks/iagent.go b/vts/policymanager/mocks/iagent.go index 9e50702a..4fa5114f 100644 --- a/vts/policymanager/mocks/iagent.go +++ b/vts/policymanager/mocks/iagent.go @@ -92,3 +92,17 @@ func (mr *MockIAgentMockRecorder) Init(v interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIAgent)(nil).Init), v) } + +// Validate mocks base method. +func (m *MockIAgent) Validate(ctx context.Context, policyRules string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", ctx, policyRules) + ret0, _ := ret[0].(error) + return ret0 +} + +// Validate indicates an expected call of Validate. +func (mr *MockIAgentMockRecorder) Validate(ctx, policyRules interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockIAgent)(nil).Validate), ctx, policyRules) +} diff --git a/vts/policymanager/mocks/ibackend.go b/vts/policymanager/mocks/ibackend.go index 857c8d22..bd89cac8 100644 --- a/vts/policymanager/mocks/ibackend.go +++ b/vts/policymanager/mocks/ibackend.go @@ -89,3 +89,17 @@ func (mr *MockIBackendMockRecorder) Init(v interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIBackend)(nil).Init), v) } + +// Validate mocks base method. +func (m *MockIBackend) Validate(ctx context.Context, policy string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", ctx, policy) + ret0, _ := ret[0].(error) + return ret0 +} + +// Validate indicates an expected call of Validate. +func (mr *MockIBackendMockRecorder) Validate(ctx, policy interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockIBackend)(nil).Validate), ctx, policy) +} From dfc047f1e7d2126011d6101eb13c7788359efdc0 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Wed, 5 Jul 2023 14:43:33 +0100 Subject: [PATCH 08/11] policy/opa: use Veraison logging Use Veraison logging framework for logging OPA engine output. Signed-off-by: Sergei Trofimov --- policy/opa.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/policy/opa.go b/policy/opa.go index c62742f1..cc5e7dce 100644 --- a/policy/opa.go +++ b/policy/opa.go @@ -8,11 +8,11 @@ import ( "encoding/json" "errors" "fmt" - "log" "github.com/open-policy-agent/opa/rego" "github.com/spf13/viper" "github.com/veraison/ear" + "github.com/veraison/services/log" ) var ErrBadOPAResult = errors.New("bad result update from policy") @@ -59,7 +59,7 @@ func (o *OPA) Evaluate( rego.Module("policy.rego", policy), rego.Input(input), rego.Query("outcome"), - rego.Dump(log.Writer()), + rego.Dump(log.NamedWriter("opa", log.DebugLevel)), ) resultSet, err := rego.Eval(ctx) From 4097df9e3dd0f6c5070e039ae3bd38f5ae067b90 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Fri, 30 Jun 2023 13:28:23 +0100 Subject: [PATCH 09/11] plugin: add IManager.GetRegisteredAttestationSchemes Add a method to retrieve the names of attestation schemes implemented by registered plugins. Signed-off-by: Sergei Trofimov --- builtin/builtin_loader.go | 16 +++ builtin/builtin_manager.go | 4 + plugin/goplugin_loader.go | 16 +++ plugin/goplugin_manager.go | 4 + plugin/imanager.go | 7 +- provisioning/api/mocks/imanager.go | 150 +++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 provisioning/api/mocks/imanager.go diff --git a/builtin/builtin_loader.go b/builtin/builtin_loader.go index a9f41f89..2f36d2a8 100644 --- a/builtin/builtin_loader.go +++ b/builtin/builtin_loader.go @@ -112,6 +112,22 @@ func GetBuiltinHandleByNameUsing[I plugin.IPluggable](ldr *BuiltinLoader, name s return handle, nil } +func GetBuiltinLoadedAttestationSchemes[I plugin.IPluggable](ldr *BuiltinLoader) []string { + schemes := make([]string, len(ldr.loadedByName)) + + i := 0 + for _, ihandle := range ldr.loadedByName { + if _, ok := ihandle.(I); !ok { + continue + } + + schemes[i] = ihandle.GetAttestationScheme() + i += 1 + } + + return schemes +} + func GetBuiltinHandleByAttestationSchemeUsing[I plugin.IPluggable]( ldr *BuiltinLoader, scheme string, diff --git a/builtin/builtin_manager.go b/builtin/builtin_manager.go index 15ec275b..8abbd745 100644 --- a/builtin/builtin_manager.go +++ b/builtin/builtin_manager.go @@ -82,6 +82,10 @@ func (o *BuiltinManager[I]) GetRegisteredMediaTypes() []string { return registeredMediatTypes } +func (o *BuiltinManager[I]) GetRegisteredAttestationSchemes() []string { + return GetBuiltinLoadedAttestationSchemes[I](o.loader) +} + func (o *BuiltinManager[I]) LookupByName(name string) (I, error) { return GetBuiltinHandleByNameUsing[I](o.loader, name) } diff --git a/plugin/goplugin_loader.go b/plugin/goplugin_loader.go index 673dc284..d771a501 100644 --- a/plugin/goplugin_loader.go +++ b/plugin/goplugin_loader.go @@ -212,6 +212,22 @@ func GetGoPluginHandleByNameUsing[I IPluggable](ldr *GoPluginLoader, name string return plugged.Handle, nil } +func GetGoPluginLoadedAttestationSchemes[I IPluggable](ldr *GoPluginLoader) []string { + schemes := make([]string, len(ldr.loadedByName)) + + i := 0 + for _, ictx := range ldr.loadedByName { + if _, ok := ictx.(*PluginContext[I]); !ok { + continue + } + + schemes[i] = ictx.GetAttestationScheme() + i += 1 + } + + return schemes +} + func GetGoPluginHandleByAttestationSchemeUsing[I IPluggable]( ldr *GoPluginLoader, scheme string, diff --git a/plugin/goplugin_manager.go b/plugin/goplugin_manager.go index be341761..5cf259e1 100644 --- a/plugin/goplugin_manager.go +++ b/plugin/goplugin_manager.go @@ -94,6 +94,10 @@ func (o *GoPluginManager[I]) GetRegisteredMediaTypes() []string { return registeredMediatTypes } +func (o *GoPluginManager[I]) GetRegisteredAttestationSchemes() []string { + return GetGoPluginLoadedAttestationSchemes[I](o.loader) +} + func (o *GoPluginManager[I]) LookupByName(name string) (I, error) { return GetGoPluginHandleByNameUsing[I](o.loader, name) } diff --git a/plugin/imanager.go b/plugin/imanager.go index a7b3de88..12591445 100644 --- a/plugin/imanager.go +++ b/plugin/imanager.go @@ -20,9 +20,14 @@ type IManager[I IPluggable] interface { IsRegisteredMediaType(mediaType string) bool // GetRegisteredMediaTypes returns a []string of media types that have - // been registered with the manger by discovered plugins. + // been registered with the manager by discovered plugins. GetRegisteredMediaTypes() []string + // GetRegisteredAttestationSchemes returns a []string of names for + // schemes that have been registered with the manager by discovered + // plugins. + GetRegisteredAttestationSchemes() []string + // LookupByMediaType returns a handle (implementation of the managed // interface) to the plugin that handles the specified mediaType. If // the mediaType is not handled by any of the registered plugins, an diff --git a/provisioning/api/mocks/imanager.go b/provisioning/api/mocks/imanager.go new file mode 100644 index 00000000..ce7d55a6 --- /dev/null +++ b/provisioning/api/mocks/imanager.go @@ -0,0 +1,150 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ../../plugin/imanager.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + plugin "github.com/veraison/services/plugin" +) + +// MockIManager is a mock of IManager interface. +type MockIManager[I plugin.IPluggable] struct { + ctrl *gomock.Controller + recorder *MockIManagerMockRecorder[I] +} + +// MockIManagerMockRecorder is the mock recorder for MockIManager. +type MockIManagerMockRecorder[I plugin.IPluggable] struct { + mock *MockIManager[I] +} + +// NewMockIManager creates a new mock instance. +func NewMockIManager[I plugin.IPluggable](ctrl *gomock.Controller) *MockIManager[I] { + mock := &MockIManager[I]{ctrl: ctrl} + mock.recorder = &MockIManagerMockRecorder[I]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIManager[I]) EXPECT() *MockIManagerMockRecorder[I] { + return m.recorder +} + +// Close mocks base method. +func (m *MockIManager[I]) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close. +func (mr *MockIManagerMockRecorder[I]) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIManager[I])(nil).Close)) +} + +// GetRegisteredAttestationSchemes mocks base method. +func (m *MockIManager[I]) GetRegisteredAttestationSchemes() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegisteredAttestationSchemes") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetRegisteredAttestationSchemes indicates an expected call of GetRegisteredAttestationSchemes. +func (mr *MockIManagerMockRecorder[I]) GetRegisteredAttestationSchemes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegisteredAttestationSchemes", reflect.TypeOf((*MockIManager[I])(nil).GetRegisteredAttestationSchemes)) +} + +// GetRegisteredMediaTypes mocks base method. +func (m *MockIManager[I]) GetRegisteredMediaTypes() []string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRegisteredMediaTypes") + ret0, _ := ret[0].([]string) + return ret0 +} + +// GetRegisteredMediaTypes indicates an expected call of GetRegisteredMediaTypes. +func (mr *MockIManagerMockRecorder[I]) GetRegisteredMediaTypes() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRegisteredMediaTypes", reflect.TypeOf((*MockIManager[I])(nil).GetRegisteredMediaTypes)) +} + +// Init mocks base method. +func (m *MockIManager[I]) Init(name string, ch *plugin.RPCChannel[I]) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", name, ch) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockIManagerMockRecorder[I]) Init(name, ch interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIManager[I])(nil).Init), name, ch) +} + +// IsRegisteredMediaType mocks base method. +func (m *MockIManager[I]) IsRegisteredMediaType(mediaType string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsRegisteredMediaType", mediaType) + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsRegisteredMediaType indicates an expected call of IsRegisteredMediaType. +func (mr *MockIManagerMockRecorder[I]) IsRegisteredMediaType(mediaType interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsRegisteredMediaType", reflect.TypeOf((*MockIManager[I])(nil).IsRegisteredMediaType), mediaType) +} + +// LookupByAttestationScheme mocks base method. +func (m *MockIManager[I]) LookupByAttestationScheme(name string) (I, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupByAttestationScheme", name) + ret0, _ := ret[0].(I) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LookupByAttestationScheme indicates an expected call of LookupByAttestationScheme. +func (mr *MockIManagerMockRecorder[I]) LookupByAttestationScheme(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupByAttestationScheme", reflect.TypeOf((*MockIManager[I])(nil).LookupByAttestationScheme), name) +} + +// LookupByMediaType mocks base method. +func (m *MockIManager[I]) LookupByMediaType(mediaType string) (I, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupByMediaType", mediaType) + ret0, _ := ret[0].(I) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LookupByMediaType indicates an expected call of LookupByMediaType. +func (mr *MockIManagerMockRecorder[I]) LookupByMediaType(mediaType interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupByMediaType", reflect.TypeOf((*MockIManager[I])(nil).LookupByMediaType), mediaType) +} + +// LookupByName mocks base method. +func (m *MockIManager[I]) LookupByName(name string) (I, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LookupByName", name) + ret0, _ := ret[0].(I) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LookupByName indicates an expected call of LookupByName. +func (mr *MockIManagerMockRecorder[I]) LookupByName(name interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LookupByName", reflect.TypeOf((*MockIManager[I])(nil).LookupByName), name) +} From adaa267e0e4b490c20c315e4f04c24e8e8f85048 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Wed, 5 Jul 2023 10:38:24 +0100 Subject: [PATCH 10/11] Initial policy management implementation Add a new service node, "management". Implement API for updating and querying policies via this node. Signed-off-by: Sergei Trofimov --- .gitignore | 1 + Makefile | 1 + deployments/docker/Makefile | 12 +- deployments/docker/deployment.cfg | 1 + deployments/docker/src/builder-dispatcher | 2 +- deployments/docker/src/config.yaml.template | 2 + deployments/docker/src/load-config.mk | 1 + deployments/docker/src/management.docker | 28 ++ deployments/docker/veraison | 27 +- integration-tests/Makefile | 5 - .../data/policies/psa-short.rego | 3 + integration-tests/data/policies/psa.rego | 9 + integration-tests/docker/bashrc | 1 + integration-tests/tests/common.yaml | 1 + .../tests/test_policy_management.tavern.yaml | 160 +++++++++ integration-tests/utils/checkers.py | 43 ++- management/Makefile | 7 + management/api/Makefile | 9 + management/api/handler.go | 307 ++++++++++++++++++ management/api/router.go | 34 ++ management/cmd/management-service/Makefile | 16 + management/cmd/management-service/config.yaml | 15 + management/cmd/management-service/main.go | 60 ++++ management/policy.go | 206 ++++++++++++ 24 files changed, 940 insertions(+), 11 deletions(-) create mode 100644 deployments/docker/src/management.docker create mode 100644 integration-tests/data/policies/psa-short.rego create mode 100644 integration-tests/data/policies/psa.rego create mode 100644 integration-tests/tests/test_policy_management.tavern.yaml create mode 100644 management/Makefile create mode 100644 management/api/Makefile create mode 100644 management/api/handler.go create mode 100644 management/api/router.go create mode 100644 management/cmd/management-service/Makefile create mode 100644 management/cmd/management-service/config.yaml create mode 100644 management/cmd/management-service/main.go create mode 100644 management/policy.go diff --git a/.gitignore b/.gitignore index ed94d71d..5f43da8a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ integration-tests/.built __generated__ __pycache__ __debug_bin +management/cmd/management-service/management-service # Test binary, built with `go test -c` *.test diff --git a/Makefile b/Makefile index fdd58d8e..4c3ffd5b 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ SUBDIR += config SUBDIR += handler SUBDIR += kvstore SUBDIR += log +SUBDIR += management SUBDIR += plugin SUBDIR += policy SUBDIR += proto diff --git a/deployments/docker/Makefile b/deployments/docker/Makefile index c551ca5d..3ca4a547 100644 --- a/deployments/docker/Makefile +++ b/deployments/docker/Makefile @@ -24,6 +24,7 @@ SRC_DIR := $(THIS_DIR)src/ BUILDER_CONTEXT := $(CONTEXT_DIR)/builder vts_FLAGS := -v $(STORES_VOLUME):/opt/veraison/stores +management_FLAGS := -v $(STORES_VOLUME):/opt/veraison/stores -p $(MANAGEMENT_PORT):$(MANAGEMENT_PORT) provisioning_FLAGS := -p $(PROVISIONING_PORT):$(PROVISIONING_PORT) verification_FLAGS := -p $(VERIFICATION_PORT):$(VERIFICATION_PORT) @@ -85,7 +86,8 @@ services: @# image targets (possibly because of the need to recursively resolve %, @# but I haven't looked too much into it). Recursively calling $(MAKE) here @# resolves the issue. - $(MAKE) .built/vts-container .built/provisioning-container .built/verification-container + $(MAKE) .built/vts-container .built/provisioning-container .built/verification-container \ + .built/management-container .PHONY: vts vts: deploy .built/vts-container @@ -105,6 +107,12 @@ verification: deploy .built/verification-container .PHONY: verification-image verification-image: deploy .built/verification-image +.PHONY: management +management: deploy .built/management-container + +.PHONY: management-image +management-image: deploy .built/management-image + .PHONY: network network: .built/network @@ -165,7 +173,7 @@ docker-clean: docker volume rm -f $(DEPLOY_DEST); \ fi @# -f ensures exit code 0, even if image doesn't exist - docker container rm -f vts-service provisioning-service verification-service + docker container rm -f vts-service provisioning-service verification-service management-service docker volume rm -f veraison-logs veraison-stores @# ubuntu uses an older version of docker without -f option for network; hence the || : cludge docker network rm $(VERAISON_NETWORK) || : diff --git a/deployments/docker/deployment.cfg b/deployments/docker/deployment.cfg index c4bb30bf..3bdd2077 100644 --- a/deployments/docker/deployment.cfg +++ b/deployments/docker/deployment.cfg @@ -12,6 +12,7 @@ VERAISON_NETWORK=veraison-net VTS_PORT=50051 PROVISIONING_PORT=8888 VERIFICATION_PORT=8080 +MANAGEMENT_PORT=8088 # Deploy destination is either an absolute path to a directory on the host, or # the name of a docker volume. diff --git a/deployments/docker/src/builder-dispatcher b/deployments/docker/src/builder-dispatcher index 3de53243..cad32bdc 100755 --- a/deployments/docker/src/builder-dispatcher +++ b/deployments/docker/src/builder-dispatcher @@ -31,7 +31,7 @@ function deploy() { cp $BUILD_DIR/provisioning/cmd/provisioning-service/provisioning-service $DEPLOY_DIR/ cp $BUILD_DIR/verification/cmd/verification-service/verification-service $DEPLOY_DIR/ cp $BUILD_DIR/vts/cmd/vts-service/vts-service $DEPLOY_DIR/ - cp $BUILD_DIR/vts/cmd/vts-service/vts-service $DEPLOY_DIR/ + cp $BUILD_DIR/management/cmd/management-service/management-service $DEPLOY_DIR/ cp $BUILD_DIR/scheme/bin/* $DEPLOY_DIR/plugins/ cp $BUILD_DIR/deployments/docker/src/skey.jwk $DEPLOY_DIR/ cp $BUILD_DIR/deployments/docker/src/service-entrypoint $DEPLOY_DIR/ diff --git a/deployments/docker/src/config.yaml.template b/deployments/docker/src/config.yaml.template index 6f1edce9..717ab91d 100644 --- a/deployments/docker/src/config.yaml.template +++ b/deployments/docker/src/config.yaml.template @@ -7,6 +7,8 @@ provisioning: listen-addr: 0.0.0.0:${PROVISIONING_PORT} verification: listen-addr: 0.0.0.0:${VERIFICATION_PORT} +management: + listen-addr: 0.0.0.0:${MANAGEMENT_PORT} vts: server-addr: vts-service:${VTS_PORT} ear-signer: diff --git a/deployments/docker/src/load-config.mk b/deployments/docker/src/load-config.mk index c9e266e8..d074c7e8 100644 --- a/deployments/docker/src/load-config.mk +++ b/deployments/docker/src/load-config.mk @@ -10,6 +10,7 @@ VERAISON_NETWORK ?= veraison-net VTS_PORT ?= 50051 PROVISIONING_PORT ?= 8888 VERIFICATION_PORT ?= 8080 +MANAGEMENT_PORT ?= 8088 # Deploy destination is either an absolute path to a directory on the host, or # the name of a docker volume. diff --git a/deployments/docker/src/management.docker b/deployments/docker/src/management.docker new file mode 100644 index 00000000..c8c7513f --- /dev/null +++ b/deployments/docker/src/management.docker @@ -0,0 +1,28 @@ +# Management service container. +# The context for building this image is assumed to be the Veraison deployment +# directory (/tmp/veraison is the default for make build). +FROM debian as veraison-management + +RUN apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install \ + --assume-yes \ + --no-install-recommends \ + uuid-runtime \ + && uuidgen | tr -d - > /etc/machine-id \ + && apt-get clean \ + && apt-get autoremove --assume-yes \ + && rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* + +RUN groupadd -g 616 veraison && \ + useradd -m -g veraison --system veraison + +USER veraison + +WORKDIR /opt/veraison + +ADD --chown=veraison:nogroup plugins plugins +ADD --chown=veraison:nogroup config.yaml management-service service-entrypoint ./ + +ENTRYPOINT ["/opt/veraison/service-entrypoint"] +CMD ["/opt/veraison/management-service"] + diff --git a/deployments/docker/veraison b/deployments/docker/veraison index 4a92c974..374dc60a 100755 --- a/deployments/docker/veraison +++ b/deployments/docker/veraison @@ -8,19 +8,21 @@ function status() { local vts=$(_get_container_state vts-service) local prov=$(_get_container_state provisioning-service) local verif=$(_get_container_state verification-service) + local manage=$(_get_container_state management-service) if [[ $_quiet == true ]]; then local vts=$(_strip_color $vts) local prov=$(_strip_color $prov) local verif=$(_strip_color $verif) + local manage=$(_strip_color $manage) local status="${_yell}stopped${_reset}" - if [[ "$vts" == "running" || "$prov" == "running" || "$verif" == "running" ]]; then + if [[ "$vts" == "running" || "$prov" == "running" || "$verif" == "running" || "$manage" == "running" ]]; then status="${_yell}partial${_yell}" fi - if [[ "$vts" == "running" && "$prov" == "running" && "$verif" == "running" ]]; then + if [[ "$vts" == "running" && "$prov" == "running" && "$verif" == "running" && "$manage" == "running" ]]; then status="${_green}running${_reset}" fi @@ -29,6 +31,7 @@ function status() { echo -e " vts: $vts" echo -e "provisioning: $prov" echo -e "verification: $verif" + echo -e " management: $manage" fi } @@ -40,12 +43,15 @@ function start() { sleep 0.5 # wait for vts to start before starting the services that depend on it. start_provisioning start_verification + start_management elif [[ "$what" == "vts" || "$what" == "vts-service" ]]; then start_vts elif [[ "$what" == "provisioning" || "$what" == "provisioning-service" ]]; then start_provisioning elif [[ "$what" == "verification" || "$what" == "verification-service" ]]; then start_verification + elif [[ "$what" == "management" || "$what" == "management-service" ]]; then + start_management else echo -e "$_error: unknown service: $what" exit 1 @@ -56,6 +62,7 @@ function stop() { local what=$1 if [[ "x$what" == "x" ]]; then + stop_management stop_verification stop_provisioning stop_vts @@ -65,6 +72,8 @@ function stop() { stop_provisioning elif [[ "$what" == "verification" || "$what" == "verification-service" ]]; then stop_verification + elif [[ "$what" == "management" || "$what" == "management-service" ]]; then + stop_management else echo -e "$_error: unknown service: $what" exit 1 @@ -80,6 +89,8 @@ function follow() { follow_provisioning elif [[ "$what" == "verification" || "$what" == "verification-service" ]]; then follow_verification + elif [[ "$what" == "management" || "$what" == "management-service" ]]; then + follow_management else echo -e "$_error: unknown service: $what" exit 1 @@ -122,6 +133,18 @@ function follow_verification() { docker container logs --follow --timestamps verification-service } +function start_management() { + docker container start management-service +} + +function stop_management() { + docker container stop management-service +} + +function follow_management() { + docker container logs --follow --timestamps management-service +} + function manager() { docker container run --rm -t \ --network veraison-net \ diff --git a/integration-tests/Makefile b/integration-tests/Makefile index 7a45d5ad..c9668391 100644 --- a/integration-tests/Makefile +++ b/integration-tests/Makefile @@ -26,11 +26,6 @@ CONTAINER_FLAGS := --env-file $(DEPLOYMENT_SRC_DIR)deployment.cfg --network ver -v $(THIS_DIR):/integration-tests \ -v $(STORES_VOLUME):/opt/veraison/stores -DEPLOYMENT_DEPS := $(DEPLOYMENT_SRC_DIR).built/network \ - $(DEPLOYMENT_SRC_DIR).built/vts-container \ - $(DEPLOYMENT_SRC_DIR).built/provisioning-container \ - $(DEPLOYMENT_SRC_DIR).built/verification-container - CLEANFILES := .pytest_cache utils/__pycache__ __generated__ .PHONY: image diff --git a/integration-tests/data/policies/psa-short.rego b/integration-tests/data/policies/psa-short.rego new file mode 100644 index 00000000..e2eaab93 --- /dev/null +++ b/integration-tests/data/policies/psa-short.rego @@ -0,0 +1,3 @@ +package policy + +executables = APPROVED_RT diff --git a/integration-tests/data/policies/psa.rego b/integration-tests/data/policies/psa.rego new file mode 100644 index 00000000..a4e2e9a4 --- /dev/null +++ b/integration-tests/data/policies/psa.rego @@ -0,0 +1,9 @@ +package policy + +executables = APPROVED_RT { + some i + + evidence["psa-software-components"][i]["measurement-type"] == "BL" + + semver_cmp(evidence["psa-software-components"][i].version, "3.5") >= 0 +} else = UNSAFE_RT diff --git a/integration-tests/docker/bashrc b/integration-tests/docker/bashrc index bf73cd4f..34e3d3c1 100644 --- a/integration-tests/docker/bashrc +++ b/integration-tests/docker/bashrc @@ -2,6 +2,7 @@ export PATH=~/.local/bin:$PATH export PYTHONPATH=$PYTHONPATH:/integration-testing/utils export PROVISIONING_HOST=provisioning-service export VERIFICATION_HOST=verification-service +export MANAGEMENT_HOST=management-service export PS1='\e[0;32m\u@debug-container \e[0;34m\w\n\e[0;32m$\e[0m ' alias ll='ls -lh --color=auto' diff --git a/integration-tests/tests/common.yaml b/integration-tests/tests/common.yaml index 200014ee..067e8c96 100644 --- a/integration-tests/tests/common.yaml +++ b/integration-tests/tests/common.yaml @@ -4,6 +4,7 @@ description: Common test information variables: provisioning-service: '{tavern.env_vars.PROVISIONING_HOST}.{tavern.env_vars.VERAISON_NETWORK}:{tavern.env_vars.PROVISIONING_PORT}' verification-service: '{tavern.env_vars.VERIFICATION_HOST}.{tavern.env_vars.VERAISON_NETWORK}:{tavern.env_vars.VERIFICATION_PORT}' + management-service: '{tavern.env_vars.MANAGEMENT_HOST}.{tavern.env_vars.VERAISON_NETWORK}:{tavern.env_vars.MANAGEMENT_PORT}' good-nonce: QUp8F0FBs9DpodKK8xUg8NQimf6sQAfe2J1ormzZLxk= bad-nonce: Ppfdfe2JzZLOk= endorsements-content-types: diff --git a/integration-tests/tests/test_policy_management.tavern.yaml b/integration-tests/tests/test_policy_management.tavern.yaml new file mode 100644 index 00000000..16b388aa --- /dev/null +++ b/integration-tests/tests/test_policy_management.tavern.yaml @@ -0,0 +1,160 @@ +test_name: policy-management + +includes: + - !include common.yaml + +stages: + - name: get active policy (non-existent) + request: + method: GET + url: http://{management-service}/management/v1/policy/PSA_IOT + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 404 + + - name: submit initial policy + request: + method: POST + url: http://{management-service}/management/v1/policy/PSA_IOT + headers: + content-type: application/vnd.veraison.policy.opa + accept: application/vnd.veraison.policy+json + file_body: data/policies/psa-short.rego + response: + status_code: 201 + save: + json: + policy-uuid: uuid + verify_response_with: + - function: checkers:check_policy + extra_kwargs: + active: false + name: default + rules_file: data/policies/psa-short.rego + + - name: get active policy (none activated) + request: + method: GET + url: http://{management-service}/management/v1/policy/PSA_IOT + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 404 + + - name: get policy by uuid + request: + method: GET + url: http://{management-service}/management/v1/policy/PSA_IOT/{policy-uuid} + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 200 + verify_response_with: + - function: checkers:check_policy + extra_kwargs: + active: false + name: default + rules_file: data/policies/psa-short.rego + + - name: activate policy + request: + method: POST + url: http://{management-service}/management/v1/policy/PSA_IOT/{policy-uuid}/activate + response: + status_code: 200 + + - name: get active policy (ok) + request: + method: GET + url: http://{management-service}/management/v1/policy/PSA_IOT + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 200 + verify_response_with: + - function: checkers:check_policy + extra_kwargs: + active: true + name: default + rules_file: data/policies/psa-short.rego + + - name: get active policy (bad scheme) + request: + method: GET + url: http://{management-service}/management/v1/policy/BAD + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 400 + json: + title: Bad Request + detail: unrecognised scheme "BAD" + + - name: submit replacement policy + request: + method: POST + url: http://{management-service}/management/v1/policy/PSA_IOT?name=test + headers: + content-type: application/vnd.veraison.policy.opa + accept: application/vnd.veraison.policy+json + file_body: data/policies/psa.rego + response: + status_code: 201 + save: + json: + second-policy-uuid: uuid + verify_response_with: + - function: checkers:check_policy + extra_kwargs: + active: false + name: test + rules_file: data/policies/psa.rego + + - name: get active policy (ok) + request: + method: GET + url: http://{management-service}/management/v1/policy/PSA_IOT + headers: + accept: application/vnd.veraison.policy+json + response: + status_code: 200 + verify_response_with: + - function: checkers:check_policy + extra_kwargs: + active: true + name: default + rules_file: data/policies/psa-short.rego + + - name: get policies (one active) + request: + method: GET + url: http://{management-service}/management/v1/policies/PSA_IOT + headers: + accept: application/vnd.veraison.policies+json + response: + status_code: 200 + verify_response_with: + - function: checkers:check_policy_list + extra_kwargs: + have_active: true + + - name: deactivate all + request: + method: POST + url: http://{management-service}/management/v1/policies/PSA_IOT/deactivate + response: + status_code: 200 + + - name: get policies (no active) + request: + method: GET + url: http://{management-service}/management/v1/policies/PSA_IOT + headers: + accept: application/vnd.veraison.policies+json + response: + status_code: 200 + verify_response_with: + - function: checkers:check_policy_list + extra_kwargs: + have_active: false diff --git a/integration-tests/utils/checkers.py b/integration-tests/utils/checkers.py index 3adca72a..8a825073 100644 --- a/integration-tests/utils/checkers.py +++ b/integration-tests/utils/checkers.py @@ -1,10 +1,10 @@ import os import json +from datetime import datetime, timedelta from jose import jwt GENDIR = '__generated__' - def save_result(response, scheme, evidence): os.makedirs(f'{GENDIR}/results', exist_ok=True) jwt_outfile = f'{GENDIR}/results/{scheme}.{evidence}.jwt' @@ -49,6 +49,43 @@ def compare_to_expected_result(response, expected, verifier_key): expected_claims["ear.veraison.policy-claims"] +def check_policy(response, active, name, rules_file): + policy = _extract_policy(response.json()) + + _check_within_period(policy['ctime'], timedelta(seconds=60)) + + if active is not None: + assert policy['active'] == active + + if name: + assert policy['name'] == name + + assert policy['type'] == 'opa' + + if rules_file: + with open(rules_file) as fh: + rules = fh.read() + + assert policy['rules'] == rules + + +def check_policy_list(response, have_active): + active_count = 0 + for entry in response.json(): + policy = _extract_policy(entry) + _check_within_period(policy['ctime'], timedelta(seconds=60)) + if policy['active']: + active_count += 1 + + assert (have_active and active_count == 1) or \ + (not have_active and active_count == 0) + + +def _check_within_period(dt, period): + now = datetime.now().replace(tzinfo=dt.tzinfo) + assert now > dt > (now - period) + + def _extract_appraisal(response, key_file): try: result = response.json()["result"] @@ -67,3 +104,7 @@ def _extract_appraisal(response, key_file): return decoded["submods"].popitem()[1] +def _extract_policy(data): + policy = data + policy['ctime'] = datetime.fromisoformat(policy['ctime']) + return policy diff --git a/management/Makefile b/management/Makefile new file mode 100644 index 00000000..332b2369 --- /dev/null +++ b/management/Makefile @@ -0,0 +1,7 @@ +# Copyright 2023 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +SUBDIR := api +SUBDIR += cmd/management-service + +include ../mk/subdir.mk diff --git a/management/api/Makefile b/management/api/Makefile new file mode 100644 index 00000000..14b04ac1 --- /dev/null +++ b/management/api/Makefile @@ -0,0 +1,9 @@ +# Copyright 2023 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +.DEFAULT_GOAL := test + +include ../../mk/common.mk +include ../../mk/pkg.mk +include ../../mk/lint.mk +include ../../mk/test.mk diff --git a/management/api/handler.go b/management/api/handler.go new file mode 100644 index 00000000..61d7e748 --- /dev/null +++ b/management/api/handler.go @@ -0,0 +1,307 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/moogar0880/problems" + "github.com/veraison/services/capability" + "github.com/veraison/services/config" + "github.com/veraison/services/log" + "github.com/veraison/services/management" + "github.com/veraison/services/policy" + "go.uber.org/zap" +) + +const ( + RulesMediaType = "application/vnd.veraison.policy.opa" + PolicyMediaType = "application/vnd.veraison.policy+json" + PoliciesMediaType = "application/vnd.veraison.policies+json" +) + +var ( + tenantID = "0" +) + +type Handler struct { + Manager *management.PolicyManager + Logger *zap.SugaredLogger +} + +func NewHandler(manager *management.PolicyManager, logger *zap.SugaredLogger) Handler { + return Handler{ + Manager: manager, + Logger: logger, + } +} + +func (o Handler) CreatePolicy(c *gin.Context) { + offered := c.NegotiateFormat(PolicyMediaType) + if offered != PolicyMediaType { + reportProblem(c, + http.StatusNotAcceptable, + fmt.Sprintf("the only supported output format is %s", + PolicyMediaType), + ) + return + } + + mediaType := c.Request.Header.Get("Content-Type") + if mediaType != RulesMediaType { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("the only supported rules format is %s", + RulesMediaType), + ) + return + } + + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + name := c.Query("name") + if name == "" { + name = "default" + } + + payload, err := io.ReadAll(c.Request.Body) + if err != nil { + reportProblem(c, http.StatusBadRequest, fmt.Sprintf("error reading body: %s", err)) + return + } + + if len(payload) == 0 { + reportProblem(c, http.StatusBadRequest, "empty body") + return + } + + policyRules := string(payload) + + if err = o.Manager.Validate(c, policyRules); err != nil { + reportProblem(c, http.StatusBadRequest, fmt.Sprintf("invalid policy: %s", err)) + } + + policy, err := o.Manager.Update(c, tenantID, scheme, name, policyRules) + if err != nil { + reportProblem(c, + http.StatusInternalServerError, + fmt.Sprintf("could not update policy: %s", err), + ) + } + + respBytes, err := json.Marshal(&policy) + if err != nil { + reportProblem(c, http.StatusInternalServerError, err.Error()) + } + + c.Data(http.StatusCreated, PolicyMediaType, respBytes) +} + +func (o Handler) GetActivePolicy(c *gin.Context) { + offered := c.NegotiateFormat(PolicyMediaType) + if offered != PolicyMediaType { + reportProblem(c, + http.StatusNotAcceptable, + fmt.Sprintf("the only supported output format is %s", + PolicyMediaType), + ) + return + } + + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + pol, err := o.Manager.GetActive(c, tenantID, scheme) + o.respondToGet(c, PolicyMediaType, pol, err) +} + +func (o Handler) GetPolicy(c *gin.Context) { + offered := c.NegotiateFormat(PolicyMediaType) + if offered != PolicyMediaType { + reportProblem(c, + http.StatusNotAcceptable, + fmt.Sprintf("the only supported output format is %s", + PolicyMediaType), + ) + return + } + + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + uuid, err := uuid.Parse(c.Param("uuid")) + if err != nil { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("bad UUID %q", c.Param("uuid")), + ) + return + } + + pol, err := o.Manager.GetPolicy(c, tenantID, scheme, uuid) + o.respondToGet(c, PolicyMediaType, pol, err) +} + +func (o Handler) GetPolicies(c *gin.Context) { + offered := c.NegotiateFormat(PoliciesMediaType) + if offered != PoliciesMediaType { + reportProblem(c, + http.StatusNotAcceptable, + fmt.Sprintf("the only supported output format is %s", + PoliciesMediaType), + ) + return + } + + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + policies, err := o.Manager.GetPolicies(c, tenantID, scheme, c.Query("name")) + o.respondToGet(c, PoliciesMediaType, policies, err) +} + +func (o Handler) Activate(c *gin.Context) { + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + uuid, err := uuid.Parse(c.Param("uuid")) + if err != nil { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("bad UUID %q", c.Param("uuid")), + ) + return + } + + err = o.Manager.Activate(c, tenantID, scheme, uuid) + o.respondSimple(c, err) +} + +func (o Handler) DeactivateAll(c *gin.Context) { + scheme := c.Param("scheme") + if !o.Manager.IsSchemeSupported(scheme) { + reportProblem(c, + http.StatusBadRequest, + fmt.Sprintf("unrecognised scheme %q", scheme), + ) + return + } + + err := o.Manager.DeactivateAll(c, tenantID, scheme) + o.respondSimple(c, err) +} + +func (o Handler) respondSimple(c *gin.Context, err error) { + if err == nil { + c.Status(http.StatusOK) + } else { + if errors.Is(err, policy.ErrNoPolicy) { + reportProblem(c, http.StatusNotFound, err.Error()) + } else { + reportProblem(c, http.StatusInternalServerError, err.Error()) + } + } +} + +func (o Handler) GetManagementWellKnownInfo(c *gin.Context) { + offered := c.NegotiateFormat(capability.WellKnownMediaType) + if offered != capability.WellKnownMediaType && offered != gin.MIMEJSON { + reportProblem(c, + http.StatusNotAcceptable, + fmt.Sprintf("the only supported output format is %s", + capability.WellKnownMediaType), + ) + return + } + + obj, err := capability.NewWellKnownInfoObj( + nil, // key + nil, // media types + o.Manager.SupportedSchemes, + config.Version, + "SERVICE_STATUS_READY", + publicApiMap, + ) + + if err != nil { + reportProblem(c, + http.StatusInternalServerError, + err.Error(), + ) + return + } + + c.Header("Content-Type", capability.WellKnownMediaType) + c.JSON(http.StatusOK, obj) + +} + +func (o Handler) respondToGet(c *gin.Context, mt string, ret interface{}, err error) { + if err != nil { + if errors.Is(err, policy.ErrNoPolicy) || errors.Is(err, policy.ErrNoActivePolicy) { + reportProblem(c, http.StatusNotFound, err.Error()) + } else { + reportProblem(c, http.StatusInternalServerError, err.Error()) + } + return + } + + respBytes, err := json.Marshal(ret) + if err != nil { + reportProblem(c, http.StatusInternalServerError, err.Error()) + } + + c.Data(http.StatusOK, mt, respBytes) +} + +func reportProblem(c *gin.Context, status int, details ...string) { + prob := problems.NewStatusProblem(status) + + if len(details) > 0 { + prob.Detail = strings.Join(details, ", ") + } + + log.LogProblem(log.Named("api"), prob) + + c.Header("Content-Type", "application/problem+json") + c.AbortWithStatusJSON(status, prob) +} diff --git a/management/api/router.go b/management/api/router.go new file mode 100644 index 00000000..9338e34c --- /dev/null +++ b/management/api/router.go @@ -0,0 +1,34 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package api + +import "github.com/gin-gonic/gin" + +var publicApiMap = map[string]string{ + "createPolicy": "/management/v1/policy/:scheme", + "activatePolicy": "/management/v1/policy/:scheme/:uuid/activate", + "getActivePolicy": "/management/v1/policy/:scheme", + "getPolicy": "/management/v1/policy/:scheme/:uuid", + "deactivatePolicies": "/management/v1/policies/:scheme/deactivate", + "getPolicies": "/management/v1/policies/:scheme", +} + +func NewRouter(handler Handler) *gin.Engine { + router := gin.New() + + router.Use(gin.Logger()) + router.Use(gin.Recovery()) + + router.POST(publicApiMap["createPolicy"], handler.CreatePolicy) + router.POST(publicApiMap["activatePolicy"], handler.Activate) + router.GET(publicApiMap["getActivePolicy"], handler.GetActivePolicy) + router.GET(publicApiMap["getPolicy"], handler.GetPolicy) + + router.POST(publicApiMap["deactivatePolicies"], handler.DeactivateAll) + router.GET(publicApiMap["getPolicies"], handler.GetPolicies) + + router.GET("/.well-known/veraison/management", handler.GetManagementWellKnownInfo) + + return router +} diff --git a/management/cmd/management-service/Makefile b/management/cmd/management-service/Makefile new file mode 100644 index 00000000..979c254a --- /dev/null +++ b/management/cmd/management-service/Makefile @@ -0,0 +1,16 @@ +# Copyright 2023 Contributors to the Veraison project. +# SPDX-License-Identifier: Apache-2.0 + +.DEFAULT_GOAL := all + +GOPKG := github.com/veraison/services/management/cmd/management-service +CMD := management-service +SRCS := main.go + +CMD_DEPS += $(wildcard ../../api/*.go) + +include ../../../mk/common.mk +include ../../../mk/cmd.mk +include ../../../mk/test.mk +include ../../../mk/lint.mk +include ../../../mk/pkg.mk diff --git a/management/cmd/management-service/config.yaml b/management/cmd/management-service/config.yaml new file mode 100644 index 00000000..f81f29a0 --- /dev/null +++ b/management/cmd/management-service/config.yaml @@ -0,0 +1,15 @@ +plugin: + backend: go-plugin + go-plugin: + dir: ../../../scheme/bin/ +po-store: + backend: sql + sql: + driver: sqlite3 + datasource: /veraison/stores/vts/po-store.sql +management: + listen-addr: 0.0.0.0:8088 +po-agent: + backend: opa +logging: + level: debug diff --git a/management/cmd/management-service/main.go b/management/cmd/management-service/main.go new file mode 100644 index 00000000..ba498104 --- /dev/null +++ b/management/cmd/management-service/main.go @@ -0,0 +1,60 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 +package main + +import ( + _ "github.com/mattn/go-sqlite3" + "github.com/veraison/services/config" + "github.com/veraison/services/log" + "github.com/veraison/services/management" + "github.com/veraison/services/management/api" +) + +var ( + DefaultListenAddr = "localhost:8088" +) + +type cfg struct { + ListenAddr string `mapstructure:"listen-addr" valid:"dialstring"` +} + +func main() { + config.CmdLine() + + v, err := config.ReadRawConfig(*config.File, false) + if err != nil { + log.Fatalf("Could not read config: %v", err) + } + + subs, err := config.GetSubs(v, "*management", "*logging") + if err != nil { + log.Fatalf("Could not parse config: %v", err) + } + + classifiers := map[string]interface{}{"service": "management"} + if err := log.Init(subs["logging"], classifiers); err != nil { + log.Fatalf("could not configure logging: %v", err) + } + log.InitGinWriter() // route gin output to our logger. + + log.Infow("Initializing Management Service", "version", config.Version) + + log.Info("initializing policy manager") + pm, err := management.CreatePolicyManagerFromConfig(v, "policy") + if err != nil { + log.Fatalf("could not init policy manager: %v", err) + } + + cfg := cfg{ListenAddr: DefaultListenAddr} + loader := config.NewLoader(&cfg) + if err := loader.LoadFromViper(subs["management"]); err != nil { + log.Fatalf("Could not load verfication config: %v", err) + + } + + log.Infow("initializing management API service", "address", cfg.ListenAddr) + handler := api.NewHandler(pm, log.Named("api")) + if err := api.NewRouter(handler).Run(cfg.ListenAddr); err != nil { + log.Fatalf("Gin engine failed: %v", err) + } +} diff --git a/management/policy.go b/management/policy.go new file mode 100644 index 00000000..891710af --- /dev/null +++ b/management/policy.go @@ -0,0 +1,206 @@ +// Copyright 2023 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package management + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/spf13/viper" + "github.com/veraison/services/builtin" + "github.com/veraison/services/config" + "github.com/veraison/services/handler" + "github.com/veraison/services/log" + "github.com/veraison/services/plugin" + "github.com/veraison/services/policy" +) + +type PolicyManager struct { + Agent policy.IAgent + Store *policy.Store + SupportedSchemes []string +} + +func CreatePolicyManagerFromConfig(v *viper.Viper, name string) (*PolicyManager, error) { + subs, err := config.GetSubs(v, "*po-agent", "po-store", "*plugin") + if err != nil { + return nil, err + } + + agent, err := policy.CreateAgent(subs["po-agent"], log.Named(name+"-agent")) + if err != nil { + return nil, err + } + + store, err := policy.NewStore(subs["po-store"], log.Named(name+"-store")) + if err != nil { + return nil, err + } + + var pluginManager plugin.IManager[handler.IEvidenceHandler] + + if config.SchemeLoader == "plugins" { // nolint:gocritic + pluginManager, err = plugin.CreateGoPluginManager( + subs["plugin"], log.Named("plugin"), + "evidence-handler", handler.EvidenceHandlerRPC) + if err != nil { + log.Fatalf("plugin manager initialization failed: %v", err) + } + } else if config.SchemeLoader == "builtin" { + pluginManager, err = builtin.CreateBuiltinManager[handler.IEvidenceHandler]( + subs["plugin"], log.Named("builtin"), "evidence-handler") + if err != nil { + log.Fatalf("scheme manager initialization failed: %v", err) + } + } else { + log.Panicw("invalid SchemeLoader value", "SchemeLoader", config.SchemeLoader) + } + defer pluginManager.Close() + + supportedSchemes := pluginManager.GetRegisteredAttestationSchemes() + + return NewPolicyManager(agent, store, supportedSchemes), nil +} + +func NewPolicyManager(agent policy.IAgent, store *policy.Store, schemes []string) *PolicyManager { + return &PolicyManager{Agent: agent, Store: store, SupportedSchemes: schemes} +} + +func (o *PolicyManager) IsSchemeSupported(scheme string) bool { + for _, supported := range o.SupportedSchemes { + if supported == scheme { + return true + } + } + + return false +} + +func (o *PolicyManager) Validate(ctx context.Context, policyRules string) error { + return o.Agent.Validate(ctx, policyRules) +} + +func (o *PolicyManager) Update( + ctx context.Context, + tenantID string, + scheme string, + name string, + rules string, +) (*policy.Policy, error) { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return nil, err + } + + return o.Store.Update(key, name, o.Agent.GetBackendName(), rules) +} + +func (o *PolicyManager) GetActive( + ctx context.Context, + tenantID string, + scheme string, +) (*policy.Policy, error) { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return nil, err + } + + return o.Store.GetActive(key) +} + +func (o *PolicyManager) GetPolicy( + ctx context.Context, + tenantID string, + scheme string, + policyID uuid.UUID, +) (*policy.Policy, error) { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return nil, err + } + + return o.Store.GetPolicy(key, policyID) +} + +func (o *PolicyManager) GetPolicies( + ctx context.Context, + tenantID string, + scheme string, + name string, +) ([]*policy.Policy, error) { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return nil, err + } + + policies, err := o.Store.Get(key) + if err != nil { + return nil, err + } + + if name == "" { + return policies, nil + } + + ret := make([]*policy.Policy, 0) + + for _, pol := range policies { + if pol.Name == name { + ret = append(ret, pol) + } + } + + return ret, nil +} + +func (o *PolicyManager) Activate( + ctx context.Context, + tenantID string, + scheme string, + policyID uuid.UUID, +) error { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return err + } + + return o.Store.Activate(key, policyID) +} + +func (o *PolicyManager) DeactivateAll( + ctx context.Context, + tenantID string, + scheme string, +) error { + key, err := o.resolvePolicyKey(tenantID, scheme) + if err != nil { + return err + } + + return o.Store.DeactivateAll(key) +} + +func (o *PolicyManager) resolvePolicyKey( + tenantID string, + scheme string, +) (policy.PolicyKey, error) { + schemeFound := false + for _, supportedScheme := range o.SupportedSchemes { + if supportedScheme == scheme { + schemeFound = true + break + } + } + + if !schemeFound { + return policy.PolicyKey{}, fmt.Errorf("Unsupported attestation scheme: %q", scheme) + } + + return policy.PolicyKey{ + TenantId: tenantID, + Scheme: scheme, + Name: o.Agent.GetBackendName(), + }, nil +} From a820514a7b2124bcf82bb56b605a1403013a1b17 Mon Sep 17 00:00:00 2001 From: Sergei Trofimov Date: Tue, 25 Jul 2023 11:03:43 +0100 Subject: [PATCH 11/11] Remove polcli and replace with pocli - Remove policy/cmd/polcli - the policy client that operated directly on the underlying stores. - Within the docker deployment, replace it with pocli (separate package) that operates on top of the management REST API. Signed-off-by: Sergei Trofimov --- deployments/docker/env.bash | 2 +- deployments/docker/env.zsh | 2 +- deployments/docker/src/builder-dispatcher | 2 +- deployments/docker/src/builder.docker | 1 + deployments/docker/src/manager-dispatcher | 6 +- deployments/docker/src/manager.docker | 5 +- deployments/docker/veraison | 6 +- policy/Makefile | 3 - policy/cmd/polcli/Makefile | 19 ----- policy/cmd/polcli/README.md | 52 ------------ policy/cmd/polcli/commands/add.go | 75 ----------------- policy/cmd/polcli/commands/common.go | 65 --------------- policy/cmd/polcli/commands/del.go | 49 ------------ policy/cmd/polcli/commands/get.go | 98 ----------------------- policy/cmd/polcli/commands/list.go | 56 ------------- policy/cmd/polcli/commands/root.go | 35 -------- policy/cmd/polcli/commands/setup.go | 23 ------ policy/cmd/polcli/example-config.yaml | 5 -- policy/cmd/polcli/main.go | 11 --- 19 files changed, 14 insertions(+), 501 deletions(-) delete mode 100644 policy/cmd/polcli/Makefile delete mode 100644 policy/cmd/polcli/README.md delete mode 100644 policy/cmd/polcli/commands/add.go delete mode 100644 policy/cmd/polcli/commands/common.go delete mode 100644 policy/cmd/polcli/commands/del.go delete mode 100644 policy/cmd/polcli/commands/get.go delete mode 100644 policy/cmd/polcli/commands/list.go delete mode 100644 policy/cmd/polcli/commands/root.go delete mode 100644 policy/cmd/polcli/commands/setup.go delete mode 100644 policy/cmd/polcli/example-config.yaml delete mode 100644 policy/cmd/polcli/main.go diff --git a/deployments/docker/env.bash b/deployments/docker/env.bash index 2631f1fc..029fd714 100644 --- a/deployments/docker/env.bash +++ b/deployments/docker/env.bash @@ -7,4 +7,4 @@ set +a alias veraison="$__VERAISON_DIR/veraison" alias cocli="$__VERAISON_DIR/veraison -- cocli" alias evcli="$__VERAISON_DIR/veraison -- evcli" -alias polcli="$__VERAISON_DIR/veraison -- polcli" +alias pocli="$__VERAISON_DIR/veraison -- pocli" diff --git a/deployments/docker/env.zsh b/deployments/docker/env.zsh index 64ecc281..13fe5be2 100644 --- a/deployments/docker/env.zsh +++ b/deployments/docker/env.zsh @@ -7,4 +7,4 @@ set +a alias veraison="$__VERAISON_DIR/veraison" alias cocli="$__VERAISON_DIR/veraison -- cocli" alias evcli="$__VERAISON_DIR/veraison -- evcli" -alias polcli="$__VERAISON_DIR/veraison -- polcli" +alias pocli="$__VERAISON_DIR/veraison -- pocli" diff --git a/deployments/docker/src/builder-dispatcher b/deployments/docker/src/builder-dispatcher index cad32bdc..c9da57e9 100755 --- a/deployments/docker/src/builder-dispatcher +++ b/deployments/docker/src/builder-dispatcher @@ -35,9 +35,9 @@ function deploy() { cp $BUILD_DIR/scheme/bin/* $DEPLOY_DIR/plugins/ cp $BUILD_DIR/deployments/docker/src/skey.jwk $DEPLOY_DIR/ cp $BUILD_DIR/deployments/docker/src/service-entrypoint $DEPLOY_DIR/ - cp $BUILD_DIR/policy/cmd/polcli/polcli $DEPLOY_DIR/utils/ cp $gobin/evcli $DEPLOY_DIR/utils/ cp $gobin/cocli $DEPLOY_DIR/utils/ + cp $gobin/pocli $DEPLOY_DIR/utils/ echo "creating config" set -a diff --git a/deployments/docker/src/builder.docker b/deployments/docker/src/builder.docker index c4609c1f..b80d713c 100644 --- a/deployments/docker/src/builder.docker +++ b/deployments/docker/src/builder.docker @@ -60,6 +60,7 @@ RUN go mod download &&\ go install github.com/mitchellh/protoc-gen-go-json@v1.1.0 &&\ go install github.com/veraison/corim/cocli@latest &&\ go install github.com/veraison/evcli/v2@latest &&\ + go install github.com/veraison/pocli@latest &&\ go install github.com/go-delve/delve/cmd/dlv@latest ADD --chown=builder:builder builder-dispatcher . diff --git a/deployments/docker/src/manager-dispatcher b/deployments/docker/src/manager-dispatcher index 32897e49..37b97297 100755 --- a/deployments/docker/src/manager-dispatcher +++ b/deployments/docker/src/manager-dispatcher @@ -57,8 +57,8 @@ function evcli() { /bin/bash -c "$cmd" } -function polcli() { - local cmd="$_utils_dir/polcli $@" +function pocli() { + local cmd="$_utils_dir/pocli $@" /bin/bash -c "$cmd" } @@ -80,6 +80,6 @@ case $command in clear-logs) clear_logs "$@";; cocli) cocli "$@";; evcli) evcli "$@";; - polcli) polcli "$@";; + pocli) pocli "$@";; *) echo -e "$_error: unexpected command: \"$command\"";; esac diff --git a/deployments/docker/src/manager.docker b/deployments/docker/src/manager.docker index db64d32f..689e85f3 100644 --- a/deployments/docker/src/manager.docker +++ b/deployments/docker/src/manager.docker @@ -32,7 +32,10 @@ USER manager WORKDIR /opt/veraison -ADD --chown=manager:nogroup utils/evcli utils/cocli utils/polcli ./utils/ +RUN mkdir -p /home/manager/.config/pocli && \ + echo "host: management-service" > /home/manager/.config/pocli/config.yaml + +ADD --chown=manager:nogroup utils/evcli utils/cocli utils/pocli ./utils/ ADD --chown=manager:nogroup manager-dispatcher ./ ENTRYPOINT ["/opt/veraison/manager-dispatcher"] diff --git a/deployments/docker/veraison b/deployments/docker/veraison index 374dc60a..cb8c213e 100755 --- a/deployments/docker/veraison +++ b/deployments/docker/veraison @@ -245,9 +245,9 @@ function evcli() { manager evcli $translated_args } -function polcli() { +function pocli() { local translated_args=$(_translate_host_paths "$@") - manager evcli $translated_args + manager pocli $translated_args } function help() { @@ -460,7 +460,7 @@ case $command in stop-tmux | kill-tmux) kill_tmux_session $2;; cocli) shift; cocli $@;; evcli) shift; evcli $@;; - polcli) shift; polcli $@;; + pocli) shift; pocli $@;; debug) manager_debug $@;; *) echo -e "$_error: unexpected command: \"$command\" (use -h for help)";; esac diff --git a/policy/Makefile b/policy/Makefile index 26d0dd09..00d60dcd 100644 --- a/policy/Makefile +++ b/policy/Makefile @@ -5,8 +5,6 @@ GOPKG := github.com/veraison/services/policy -SUBDIR := cmd/polcli - INTERFACES := ibackend.go MOCKPKG := mocks @@ -18,4 +16,3 @@ include ../mk/common.mk include ../mk/pkg.mk include ../mk/lint.mk include ../mk/test.mk -include ../mk/subdir.mk diff --git a/policy/cmd/polcli/Makefile b/policy/cmd/polcli/Makefile deleted file mode 100644 index 0012b0ce..00000000 --- a/policy/cmd/polcli/Makefile +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2022 Contributors to the Veraison project. -# SPDX-License-Identifier: Apache-2.0 - -.DEFAULT_GOAL := all - -GOPKG := github.com/veraison/services/policy/cmd/polcli -CMD := polcli -SRCS := main.go $(wildcard commands/*.go) - -CMD_DEPS += $(wildcard ../../*.go) - -cmd-hook-pre test-hook-pre lint-hook-pre: - $(MAKE) -C ../../../proto protogen - -include ../../../mk/common.mk -include ../../../mk/cmd.mk -include ../../../mk/test.mk -include ../../../mk/lint.mk -include ../../../mk/pkg.mk diff --git a/policy/cmd/polcli/README.md b/policy/cmd/polcli/README.md deleted file mode 100644 index e4b1e471..00000000 --- a/policy/cmd/polcli/README.md +++ /dev/null @@ -1,52 +0,0 @@ -This is CLI interface to the policy store. It allows typical CRUD operations on -the store as well as listing all stored policies (use -h flag for more details -of available commands). - -Connection to the store is configured by "po-store" entry in a "config.yaml" in -the current directory (see the included example file). Alternatively, an -sqlite3 database file can be specified with -s/--store. - - -## Examples - -Perform a one-time setup of a new store - - ./polcli setup - -(For an SQL-backed store, this will create the required table.) - -Add policy from a file under the specified ID: - - ./polcli add opa://1 path/to/policy.rego - -(Note: ID must be in the form "opa://", where is the -integer ID of the tenant for whom the policy will be added. The "opa://" prefix -indicates the policy format; currently, only OPA rego policies are supported.) - -Update and existing ID with a new version (or add if ID doesn't already exist): - - ./polcli add -u opa://1 path/to/newpolicy.rego - -List stored policies: - - ./polcli list - -(The versions listed are the latest associated with the corresponding ID. -Alternatively, -a flag can be used to list all stored versions.) - -Print policy stored under the specified ID to STDOUT: - - ./polcli get opa://1 - -(This will print the latest version. -v flag can be used to specify an earlier -version. -o flag can be used to specify a file to write to, instead of printing -to STDOUT.) - - -Delete policy with the specified ID: - - ./polcli del opa://1 - -(This will delete all versions associated with ID from the store.) - - diff --git a/policy/cmd/polcli/commands/add.go b/policy/cmd/polcli/commands/add.go deleted file mode 100644 index 6456a489..00000000 --- a/policy/cmd/polcli/commands/add.go +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "fmt" - "log" - "os" - - _ "github.com/mattn/go-sqlite3" - "github.com/spf13/cobra" - - "github.com/veraison/services/policy" -) - -var ( - addCmd = &cobra.Command{ - Use: "add [-s STORE] ID FILE", - Short: "add a new policy, or update an existing one under the specified ID", - Args: cobra.MatchAll(cobra.ExactArgs(2), validateAddArgs), - RunE: doAddCommand, - PreRunE: initPolicyStore, - PostRunE: finiPolicyStore, - } - - shouldUpdate bool -) - -func init() { - addCmd.PersistentFlags().BoolVarP(&shouldUpdate, "update", "u", false, - "if specfied, the policy will be updated if it already exists") -} - -func validateAddArgs(cmd *cobra.Command, args []string) error { - // note: assumes ExactArgs(2) matched. - - if _, err := policy.PolicyKeyFromString(args[0]); err != nil { - return fmt.Errorf("invalid policy ID: %w", err) - } - - if _, err := os.Stat(args[1]); err != nil { - return fmt.Errorf("could not stat policy file: %w", err) - } - - return nil -} - -func doAddCommand(cmd *cobra.Command, args []string) error { - policyID, err := policy.PolicyKeyFromString(args[0]) - if err != nil { - return err - } - - policyFile := args[1] - - rulesBytes, err := os.ReadFile(policyFile) - if err != nil { - return fmt.Errorf("could not read policy: %w", err) - } - - addFunc := store.Add - if shouldUpdate { - addFunc = store.Update - } - - policy, err := addFunc(policyID, "default", "opa", string(rulesBytes)) - if err != nil { - return fmt.Errorf("could not add policy: %w", err) - } - - log.Printf("Policy %q stored under key %q with UUID %q .\n", - policyFile, policyID, policy.UUID) - - return nil -} diff --git a/policy/cmd/polcli/commands/common.go b/policy/cmd/polcli/commands/common.go deleted file mode 100644 index 45599555..00000000 --- a/policy/cmd/polcli/commands/common.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "fmt" - - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/veraison/services/config" - "github.com/veraison/services/log" - "github.com/veraison/services/policy" -) - -var ( - rawConfig *viper.Viper - store *policy.Store - storeDsnFromFlag string - - storeDefaults = map[string]string{ - "backend": "sql", - "sql.driver": "sqlite3", - "sql.datasource": "po-store.sql", - } -) - -func init() { - cobra.OnInitialize(initConfig) -} - -func initConfig() { - var err error - rawConfig, err = config.ReadRawConfig(cfgFile, true) - cobra.CheckErr(err) -} - -func initPolicyStore(cmd *cobra.Command, args []string) error { - cfg := rawConfig.Sub("po-store") - for k, v := range storeDefaults { - cfg.SetDefault(k, v) - } - - // if store location has been specified with --store flag, set it as - // the datasource for the selected backend. - if storeDsnFromFlag != "" { - cfg.Set(fmt.Sprintf("%s.datasource", cfg.GetString("backend")), storeDsnFromFlag) - } - - var err error - - store, err = policy.NewStore(cfg, log.Named("po-store")) - if err != nil { - return fmt.Errorf("could not initialize policy store: %w", err) - } - - return nil -} - -func finiPolicyStore(cmd *cobra.Command, args []string) error { - if store != nil { - store.Close() - } - - return nil -} diff --git a/policy/cmd/polcli/commands/del.go b/policy/cmd/polcli/commands/del.go deleted file mode 100644 index ba81973a..00000000 --- a/policy/cmd/polcli/commands/del.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "fmt" - "log" - - _ "github.com/mattn/go-sqlite3" - "github.com/spf13/cobra" - - "github.com/veraison/services/policy" -) - -var ( - delCmd = &cobra.Command{ - Use: "del [-s STORE] ID", - Short: "delete the policy with the specified ID", - Args: cobra.MatchAll(cobra.ExactArgs(1), validateDelArgs), - RunE: doDelCommand, - PreRunE: initPolicyStore, - PostRunE: finiPolicyStore, - } -) - -func validateDelArgs(cmd *cobra.Command, args []string) error { - // note: assumes ExactArgs(1) matched. - - if _, err := policy.PolicyKeyFromString(args[0]); err != nil { - return fmt.Errorf("invalid policy ID: %w", err) - } - - return nil -} - -func doDelCommand(cmd *cobra.Command, args []string) error { - policyID, err := policy.PolicyKeyFromString(args[0]) - if err != nil { - return err - } - - if err := store.Del(policyID); err != nil { - return fmt.Errorf("could not delete policy: %w", err) - } - - log.Printf("Policy %q deleted.\n", policyID) - - return nil -} diff --git a/policy/cmd/polcli/commands/get.go b/policy/cmd/polcli/commands/get.go deleted file mode 100644 index e17bb097..00000000 --- a/policy/cmd/polcli/commands/get.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "fmt" - "os" - - _ "github.com/mattn/go-sqlite3" - "github.com/spf13/cobra" - "github.com/veraison/services/policy" -) - -var ( - getCmd = &cobra.Command{ - Use: "get [-s STORE] [-v VERSION] ID", - Short: "get the policy stored under the specified ID", - Args: cobra.MatchAll(cobra.ExactArgs(1), validateGetArgs), - RunE: doGetCommand, - PreRunE: initPolicyStore, - PostRunE: finiPolicyStore, - } - - getUUID string - getOutputFilePath string -) - -func init() { - getCmd.PersistentFlags().StringVarP(&getUUID, "version", "v", "", - "get the specified, rather than latest, version") - getCmd.PersistentFlags().StringVarP(&getOutputFilePath, "output", "o", "", - "write the policy to the specified file, rather than STDOUT") -} - -func validateGetArgs(cmd *cobra.Command, args []string) error { - // note: assumes ExactArgs(1) matched. - - if _, err := policy.PolicyKeyFromString(args[0]); err != nil { - return fmt.Errorf("invalid policy ID: %w", err) - } - - return nil -} - -func doGetCommand(cmd *cobra.Command, args []string) error { - var policies []*policy.Policy - var pol *policy.Policy - var err error - - policyKey, err := policy.PolicyKeyFromString(args[0]) - if err != nil { - return err - } - - if getUUID == "" { - pol, err = store.GetActive(policyKey) - if err != nil { - return err - } - } else { - policies, err = store.Get(policyKey) - if err != nil { - return err - } - - found := false - for _, candidate := range policies { - if candidate.UUID.String() == getUUID { - pol = candidate - found = true - break - } - } - - if !found { - return fmt.Errorf("UUID %q for policy %q not found", - getUUID, policyKey) - } - } - - var writer *os.File - - if getOutputFilePath != "" { - writer, err = os.Create(getOutputFilePath) - if err != nil { - return fmt.Errorf("Could not open %q for writing: %w", - getOutputFilePath, err) - } - } else { - writer = os.Stdout - } - - if _, err := writer.Write([]byte(pol.Rules)); err != nil { - return err - } - - return nil -} diff --git a/policy/cmd/polcli/commands/list.go b/policy/cmd/polcli/commands/list.go deleted file mode 100644 index f1c432e7..00000000 --- a/policy/cmd/polcli/commands/list.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "crypto/md5" - "fmt" - "os" - - _ "github.com/mattn/go-sqlite3" - "github.com/olekukonko/tablewriter" - "github.com/spf13/cobra" -) - -var ( - listCmd = &cobra.Command{ - Use: "list [-s STORE]", - Short: "list policies in the store", - Args: cobra.NoArgs, - RunE: doListCommand, - PreRunE: initPolicyStore, - PostRunE: finiPolicyStore, - } - - shouldListAll bool -) - -func init() { - listCmd.PersistentFlags().BoolVarP(&shouldListAll, "all", "a", false, - "if specfied, all stored versions of policies will be listed") -} - -func doListCommand(cmd *cobra.Command, args []string) error { - listFunc := store.List - if shouldListAll { - listFunc = store.ListAllVersions - } - - policies, err := listFunc() - if err != nil { - return fmt.Errorf("could not list policies: %w", err) - } - - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader([]string{"id", "version", "md5sum"}) - - for _, p := range policies { - uuid := p.UUID.String() - md5sum := fmt.Sprintf("%x", md5.Sum([]byte(p.Rules))) - table.Append([]string{p.StoreKey.String(), uuid, md5sum}) - } - - table.Render() - - return nil -} diff --git a/policy/cmd/polcli/commands/root.go b/policy/cmd/polcli/commands/root.go deleted file mode 100644 index e8b06fb6..00000000 --- a/policy/cmd/polcli/commands/root.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "github.com/spf13/cobra" - - "github.com/veraison/services/config" -) - -var ( - cfgFile string - - rootCmd = &cobra.Command{ - Use: "polcli", - Short: "policy management client", - Version: config.Version, - } -) - -func Execute() { - cobra.CheckErr(rootCmd.Execute()) -} - -func init() { - rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file") - rootCmd.PersistentFlags().StringVarP(&storeDsnFromFlag, "store", "s", "", - "policy store datasource (only used for sql backend).") - - rootCmd.AddCommand(setupCmd) - rootCmd.AddCommand(addCmd) - rootCmd.AddCommand(listCmd) - rootCmd.AddCommand(getCmd) - rootCmd.AddCommand(delCmd) -} diff --git a/policy/cmd/polcli/commands/setup.go b/policy/cmd/polcli/commands/setup.go deleted file mode 100644 index f4d1bd0e..00000000 --- a/policy/cmd/polcli/commands/setup.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package commands - -import ( - "github.com/spf13/cobra" -) - -var ( - setupCmd = &cobra.Command{ - Use: "setup [-s STORE]", - Short: "one-time setup for a new store.", - Long: "Perform a one-time setup of the store. What this entails is backend-dependent (e.g. the sql backend will create the table used by the store.", - Args: cobra.NoArgs, - RunE: doSetupCommand, - PreRunE: initPolicyStore, - PostRunE: finiPolicyStore, - } -) - -func doSetupCommand(cmd *cobra.Command, args []string) error { - return store.Setup() -} diff --git a/policy/cmd/polcli/example-config.yaml b/policy/cmd/polcli/example-config.yaml deleted file mode 100644 index f421e321..00000000 --- a/policy/cmd/polcli/example-config.yaml +++ /dev/null @@ -1,5 +0,0 @@ -po-store: - backend: sql - sql: - driver: sqlite3 - datasource: po-store.sql diff --git a/policy/cmd/polcli/main.go b/policy/cmd/polcli/main.go deleted file mode 100644 index b4584f07..00000000 --- a/policy/cmd/polcli/main.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022-2023 Contributors to the Veraison project. -// SPDX-License-Identifier: Apache-2.0 -package main - -import ( - "github.com/veraison/services/policy/cmd/polcli/commands" -) - -func main() { - commands.Execute() -}